From f317ee7fd7b10d0adb4cc4a5b21395836c27f8e0 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 17 Feb 2025 14:00:50 +0100 Subject: [PATCH 1/9] [NAE-1843] Import/Export services - added CaseExporter class - implementation of case export - work in progress --- .../PrototypesConfiguration.java | 8 +- .../engine/workflow/service/CaseExporter.java | 331 ++++++++++++++++++ 2 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java diff --git a/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java index f5ec750ded1..b9d49c339bd 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java @@ -10,10 +10,10 @@ import com.netgrif.application.engine.pdf.generator.service.interfaces.IPdfGenerator; import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate; import com.netgrif.application.engine.workflow.domain.FileStorageConfiguration; +import com.netgrif.application.engine.workflow.service.CaseExporter; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; @Configuration @@ -60,4 +60,10 @@ public IPdfDrawer pdfDrawer() { public UserResourceAssembler userResourceAssembler() { return new UserResourceAssembler(); } + + @Bean("caseExporter") + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public CaseExporter caseExporter() { + return new CaseExporter(); + } } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java new file mode 100644 index 00000000000..b3c6c16885b --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java @@ -0,0 +1,331 @@ +package com.netgrif.application.engine.workflow.service; + +import com.netgrif.application.engine.importer.model.*; +import com.netgrif.application.engine.importer.model.Properties; +import com.netgrif.application.engine.petrinet.domain.I18nString; +import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior; +import com.netgrif.application.engine.workflow.domain.triggers.AutoTrigger; +import com.netgrif.application.engine.workflow.domain.triggers.TimeTrigger; +import com.netgrif.application.engine.workflow.domain.triggers.Trigger; +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.OutputStream; +import java.math.BigInteger; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Slf4j +public class CaseExporter { + + @Autowired + private ITaskService taskService; + + private final ObjectFactory objectFactory = new ObjectFactory(); + + public void exportCases(Collection casesToExport, OutputStream outputStream) { + casesToExport.forEach(caseToExport -> exportCase(caseToExport, outputStream)); + } + + public void exportCase(com.netgrif.application.engine.workflow.domain.Case caseToExport, OutputStream outputStream) { + Case xmlCase = objectFactory.createCase(); + exportCaseMetadata(caseToExport, xmlCase); + List tasks = caseToExport.getTasks().stream() + .map(taskPair -> taskService.findOne(taskPair.getTask())) + .toList(); + exportTasks(tasks, xmlCase.getTask()); + exportDataFields(caseToExport.getDataSet(), xmlCase.getDataField()); + try { + marshallCase(xmlCase, outputStream); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + private void exportDataFields(LinkedHashMap dataSet, List dataField) { + dataSet.forEach((key, value) -> dataField.add(exportDataField(key, value))); + } + + private DataField exportDataField(String fieldId, com.netgrif.application.engine.workflow.domain.DataField dataFieldToExport) { + DataField xmlDataField = objectFactory.createDataField(); + xmlDataField.setId(fieldId); + xmlDataField.setValue(exportDataFieldValue(dataFieldToExport.getValue())); + xmlDataField.setFilterMetadata(exportDataFieldValue(dataFieldToExport.getFilterMetadata())); + xmlDataField.setEncryption(dataFieldToExport.getEncryption()); + xmlDataField.setLastModified(exportLocalDateTime(dataFieldToExport.getLastModified())); + xmlDataField.setVersion(dataFieldToExport.getVersion()); + xmlDataField.setAllowedNets(exportCollectionOfStrings(dataFieldToExport.getAllowedNets())); + xmlDataField.setComponent(exportComponent(dataFieldToExport.getComponent())); + xmlDataField.getDataRefComponent().addAll(exportDataRefComponents(dataFieldToExport.getDataRefComponents())); + xmlDataField.setValidations(exportValidations(dataFieldToExport.getValidations())); + xmlDataField.setOptions(exportOptions(dataFieldToExport.getOptions())); + xmlDataField.setBehaviors(exportTaskBehaviors(dataFieldToExport.getBehavior())); + return xmlDataField; + } + + private TaskBehaviors exportTaskBehaviors(Map> taskBehavior) { + if (taskBehavior == null || taskBehavior.isEmpty()) { + return null; + } + TaskBehaviors xmlTaskBehaviors = new TaskBehaviors(); + taskBehavior.forEach((taskId, behaviors) -> { + Behaviors xmlBehavior = objectFactory.createBehaviors(); + xmlBehavior.setTaskId(taskId); + behaviors.forEach(behavior -> { + xmlBehavior.getBehavior().add(Behavior.fromValue(behavior.toString())); + }); + xmlTaskBehaviors.getTaskBehavior().add(xmlBehavior); + }); + return xmlTaskBehaviors; + } + + private Options exportOptions(Map options) { + if (options == null || options.isEmpty()) { + return null; + } + Options xmlOptions = objectFactory.createOptions(); + options.forEach((key, option) -> { + Option xmlOption = objectFactory.createOption(); + xmlOption.setKey(key); + xmlOption.setName(option.getKey()); + xmlOption.setValue(option.getDefaultValue()); + xmlOptions.getOption().add(xmlOption); + }); + return xmlOptions; + } + + private Validations exportValidations(List validations) { + if (validations == null || validations.isEmpty()) { + return null; + } + Validations xmlValidations = objectFactory.createValidations(); + validations.forEach(validation -> { +// todo resolve other Validation object properties after 6.4.0 is merged into 7.0.0 + Validation xmlValidation = objectFactory.createValidation(); + xmlValidation.setMessage(exportI18NString(validation.getValidationMessage())); + xmlValidations.getValidation().add(xmlValidation); + }); + return xmlValidations; + } + + private List exportDataRefComponents(Map dataRefComponents) { + if (dataRefComponents == null || dataRefComponents.isEmpty()) { + return null; + } + List xmlDataRefComponents = new ArrayList<>(); + dataRefComponents.forEach((taskId, component) -> { + DataRefComponent xmlDataRefComponent = objectFactory.createDataRefComponent(); + Component xmlComponent = exportComponent(component); + xmlDataRefComponent.setTaskId(taskId); + xmlDataRefComponents.add(xmlDataRefComponent); + }); + return xmlDataRefComponents; + } + + private Component exportComponent(com.netgrif.application.engine.petrinet.domain.Component component) { + if (component == null) { + return null; + } + Component xmlComponent = objectFactory.createComponent(); + xmlComponent.setName(component.getName()); + xmlComponent.setProperties(exportProperties(component.getProperties(), component.getOptionIcons())); + return xmlComponent; + } + + private Properties exportProperties(Map properties, List optionIcons) { + if ((properties == null || properties.isEmpty()) && (optionIcons == null || optionIcons.isEmpty())) { + return null; + } + Properties xmlProperties = objectFactory.createProperties(); + if (properties != null) { + properties.forEach((key, value) -> { + Property xmlProperty = objectFactory.createProperty(); + xmlProperty.setKey(key); + xmlProperty.setValue(value); + xmlProperties.getProperty().add(xmlProperty); + }); + } + if (optionIcons != null) { + optionIcons.forEach((icon) -> { + Icons xmlIcons = objectFactory.createIcons(); + Icon xmlIcon = objectFactory.createIcon(); + xmlIcon.setKey(icon.getKey()); + xmlIcon.setValue(icon.getValue()); + xmlIcon.setType(IconType.fromValue(icon.getType())); + xmlProperties.setOptionIcons(xmlIcons); + }); + } + return xmlProperties; + } + + private String exportDataFieldValue(Object value) { + if (value == null) { + return null; + } + return value.toString(); + } + + private void exportTasks(List tasksToExport, List xmlTasks) { + tasksToExport.forEach(taskToExport -> xmlTasks.add(exportTask(taskToExport))); + } + + private Task exportTask(com.netgrif.application.engine.workflow.domain.Task taskToExport) { + Task xmlTask = objectFactory.createTask(); + xmlTask.setId(taskToExport.getStringId()); + xmlTask.setTransitionId(taskToExport.getTransitionId()); + xmlTask.setTitle(exportI18NString(taskToExport.getTitle())); + xmlTask.setPriority(BigInteger.valueOf(taskToExport.getPriority())); + xmlTask.setUserId(taskToExport.getUserId()); + xmlTask.setStartDate(exportLocalDateTime(taskToExport.getStartDate())); + xmlTask.setFinishDate(exportLocalDateTime(taskToExport.getFinishDate())); + xmlTask.setFinishedBy(taskToExport.getFinishedBy()); + xmlTask.setTransactionId(taskToExport.getTransactionId()); + xmlTask.setIcon(taskToExport.getIcon()); + xmlTask.setAssignPolicy(AssignPolicy.fromValue(taskToExport.getAssignPolicy().toString())); + xmlTask.setDataFocusPolicy(DataFocusPolicy.fromValue(taskToExport.getDataFocusPolicy().toString())); + xmlTask.setFinishPolicy(FinishPolicy.fromValue(taskToExport.getFinishPolicy().toString())); + xmlTask.setTags(exportTags(taskToExport.getTags())); + xmlTask.setViewRoles(exportCollectionOfStrings(taskToExport.getViewRoles())); + xmlTask.setViewUserRefs(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setNegativeViewRoles(exportCollectionOfStrings(taskToExport.getNegativeViewRoles())); + xmlTask.setNegativeViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setImmediateDataFields(exportCollectionOfStrings(taskToExport.getImmediateDataFields())); + xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); + xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); + xmlTask.setUser(exportPermissions(taskToExport.getUsers())); + xmlTask.setAssignedUserPolicies(exportAssignedUserPolicy(taskToExport.getAssignedUserPolicy())); + xmlTask.setTriggers(exportTriggers(taskToExport.getTriggers())); + return xmlTask; + } + + private TaskTrigger exportTriggers(List triggers) { + TaskTrigger taskTrigger = objectFactory.createTaskTrigger(); + triggers.forEach(trigger -> { + TriggerWithId xmlTrigger = objectFactory.createTriggerWithId(); + xmlTrigger.setId(trigger.get_id().toString()); + TriggerType triggerTypeString; + if (trigger instanceof AutoTrigger) { + triggerTypeString = TriggerType.AUTO; + } else if (trigger instanceof TimeTrigger) { + triggerTypeString = TriggerType.TIME; + } else { + triggerTypeString = TriggerType.USER; + } + xmlTrigger.setType(triggerTypeString); + taskTrigger.setTrigger(xmlTrigger); + }); + return taskTrigger; + } + + private AssignedUserPolicies exportAssignedUserPolicy(Map assignedUserPolicy) { + AssignedUserPolicies assignedUserPolicies = objectFactory.createAssignedUserPolicies(); + assignedUserPolicies.getAssignedUserPolicy().addAll(exportBooleanMap(assignedUserPolicy)); + return assignedUserPolicies; + } + + protected void marshallCase(com.netgrif.application.engine.importer.model.Case caseToExport, OutputStream outputStream) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(com.netgrif.application.engine.importer.model.Case.class); + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); +// todo extract to property? + marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "https://petriflow.com/petriflow.schema.xsd"); + marshaller.marshal(caseToExport, outputStream); + } + + private void exportCaseMetadata(com.netgrif.application.engine.workflow.domain.Case caseToExport, Case xmlCase) { + xmlCase.setId(caseToExport.getStringId()); +// todo should the whole object be exported? is id specific enough? should email be exported instead of id? + xmlCase.setAuthor(caseToExport.getAuthor().getId()); + xmlCase.setColor(caseToExport.getColor()); + xmlCase.setProcessVersion(caseToExport.getPetriNet().getVersion().toString()); + xmlCase.setProcessIdentifier(xmlCase.getProcessIdentifier()); + xmlCase.setVisualId(xmlCase.getVisualId()); + xmlCase.setUriNodeId(xmlCase.getUriNodeId()); + xmlCase.setTags(exportTags(caseToExport.getTags())); + xmlCase.setTitle(caseToExport.getTitle()); + xmlCase.setCreationDate(exportLocalDateTime(caseToExport.getCreationDate())); + xmlCase.setLastModified(exportLocalDateTime(caseToExport.getLastModified())); + xmlCase.setEnabledRoles(exportCollectionOfStrings(caseToExport.getEnabledRoles())); + xmlCase.setViewRoles(exportCollectionOfStrings(caseToExport.getViewRoles())); + xmlCase.setViewUserRefs(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + xmlCase.setViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + xmlCase.setNegativeViewRoles(exportCollectionOfStrings(caseToExport.getNegativeViewRoles())); + xmlCase.setNegativeViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + xmlCase.setImmediateDataFields(exportCollectionOfStrings(caseToExport.getImmediateDataFields())); + xmlCase.setActivePlaces(exportMapXsdType(caseToExport.getActivePlaces())); + xmlCase.setConsumedTokens(exportMapXsdType(caseToExport.getConsumedTokens())); + xmlCase.setPermissions(exportPermissions(caseToExport.getPermissions())); + xmlCase.setUserRefs(exportPermissions(caseToExport.getUserRefs())); + xmlCase.setUsers(exportPermissions(caseToExport.getUsers())); + } + + private Tags exportTags(Map tags) { + if (tags == null || tags.isEmpty()) { + return null; + } + Tags xmlTags = objectFactory.createTags(); + tags.forEach((key, value) -> { + Tag tag = new Tag(); + tag.setKey(key); + tag.setValue(value); + xmlTags.getTag().add(tag); + }); + return xmlTags; + } + + private I18NStringType exportI18NString(I18nString i18nString) { + I18NStringType i18NStringType = objectFactory.createI18NStringType(); + i18NStringType.setName(i18nString.getKey()); + i18NStringType.setValue(i18nString.getDefaultValue()); + return i18NStringType; + } + + private String exportLocalDateTime(LocalDateTime toExport) { + return toExport == null ? null : toExport.format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } + + private StringCollection exportCollectionOfStrings(Collection collection) { + StringCollection stringCollection = objectFactory.createStringCollection(); + stringCollection.getValue().addAll(collection); + return stringCollection; + } + + private MapXsdType exportMapXsdType(Map map) { + MapXsdType xsdType = objectFactory.createMapXsdType(); + map.forEach((key, value) -> { + IntegerMapEntry mapEntry = objectFactory.createIntegerMapEntry(); + mapEntry.setKey(key); + mapEntry.setValue(BigInteger.valueOf(value)); + xsdType.getEntry().add(mapEntry); + }); + return xsdType; + } + + private PermissionMap exportPermissions(Map> permissions) { + PermissionMap permissionMap = objectFactory.createPermissionMap(); + permissions.forEach((key, value) -> { + Permissions permission = objectFactory.createPermissions(); + permission.setId(key); + permission.getPermission().addAll(exportBooleanMap(value)); + permissionMap.getEntry().add(permission); + }); + return permissionMap; + } + + private List exportBooleanMap(Map toExport) { + List booleanMapEntryList = new ArrayList<>(); + toExport.forEach((id, permissionValue) -> { + BooleanMapEntry mapEntry = objectFactory.createBooleanMapEntry(); + mapEntry.setKey(id); + mapEntry.setValue(permissionValue); + booleanMapEntryList.add(mapEntry); + }); + return booleanMapEntryList; + } +} From 0c106a20129766aa419f695b04ddde253a88fe6a Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Tue, 11 Mar 2025 12:12:17 +0100 Subject: [PATCH 2/9] [NAE-1843] Import/Export services - added CaseImporter - added service CaseImportExportService and controller CaseImportExportController - added schema location to properties - functionality changes to CaseExporter - added full xsd schema to petriflow_schema.xsd for testing purposes - added new dependecy zip4j - work in progress --- pom.xml | 6 + .../engine/archive/ZipService.java | 31 + .../archive/interfaces/IArchiveService.java | 16 + .../PrototypesConfiguration.java | 6 + .../engine/importer/service/FieldFactory.java | 9 +- .../engine/importer/service/Importer.java | 15 +- .../engine/utils/ImporterUtils.java | 27 + .../engine/workflow/domain/Case.java | 6 +- .../engine/workflow/service/CaseExporter.java | 258 ++++- .../service/CaseImportExportService.java | 91 ++ .../engine/workflow/service/CaseImporter.java | 446 ++++++++ .../interfaces/ICaseExportImportService.java | 26 + .../web/CaseImportExportController.java | 59 + src/main/resources/application-dev.properties | 5 +- src/main/resources/application.properties | 3 + .../resources/petriNets/petriflow_schema.xsd | 1010 ++++++++++++++++- .../engine/workflow/CaseExporterTest.java | 89 ++ 17 files changed, 2022 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/archive/ZipService.java create mode 100644 src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java create mode 100644 src/main/java/com/netgrif/application/engine/utils/ImporterUtils.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java create mode 100644 src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java diff --git a/pom.xml b/pom.xml index 5879b7169af..c227ca70b41 100644 --- a/pom.xml +++ b/pom.xml @@ -568,6 +568,12 @@ minio 8.5.12 + + + net.lingala.zip4j + zip4j + 2.11.5 + diff --git a/src/main/java/com/netgrif/application/engine/archive/ZipService.java b/src/main/java/com/netgrif/application/engine/archive/ZipService.java new file mode 100644 index 00000000000..28d2ef5ff42 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/archive/ZipService.java @@ -0,0 +1,31 @@ +package com.netgrif.application.engine.archive; + +import com.netgrif.application.engine.archive.interfaces.IArchiveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.io.*; + +@Slf4j +@Service +public class ZipService implements IArchiveService { + + @Override + public void pack(String archivePath, String... filePaths) throws FileNotFoundException { + this.pack(new FileOutputStream(archivePath), filePaths); + } + + @Override + public void pack(OutputStream archiveStream, String... filePaths) { + } + + @Override + public void unpack(String archivePath, String outputPath) { + + } + + @Override + public void unpack(InputStream archiveStream, String outputPath) { + + } +} diff --git a/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java b/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java new file mode 100644 index 00000000000..506d02c3860 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java @@ -0,0 +1,16 @@ +package com.netgrif.application.engine.archive.interfaces; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface IArchiveService { + + void pack(String archivePath, String... filePaths) throws FileNotFoundException; + + void pack(OutputStream archiveStream, String... filePaths); + + void unpack(String archivePath, String outputPath); + + void unpack(InputStream archiveStream, String outputPath); +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java b/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java index b9d49c339bd..150fcbd69fc 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java +++ b/src/main/java/com/netgrif/application/engine/configuration/PrototypesConfiguration.java @@ -11,6 +11,7 @@ import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate; import com.netgrif.application.engine.workflow.domain.FileStorageConfiguration; import com.netgrif.application.engine.workflow.service.CaseExporter; +import com.netgrif.application.engine.workflow.service.CaseImporter; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -66,4 +67,9 @@ public UserResourceAssembler userResourceAssembler() { public CaseExporter caseExporter() { return new CaseExporter(); } + @Bean("caseImporter") + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public CaseImporter caseImporter() { + return new CaseImporter(); + } } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java b/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java index 4ac4a03c32c..481488da0e6 100644 --- a/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java +++ b/src/main/java/com/netgrif/application/engine/importer/service/FieldFactory.java @@ -12,6 +12,7 @@ import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.runner.Expression; import com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.DynamicValidation; import com.netgrif.application.engine.petrinet.domain.views.View; +import com.netgrif.application.engine.utils.ImporterUtils; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.DataField; import com.netgrif.application.engine.workflow.service.interfaces.IDataValidationExpressionEvaluator; @@ -276,13 +277,13 @@ Field getField(Data data, Importer importer) throws IllegalArgumentException, Mi if (data.getValid() != null) { List list = data.getValid(); for (Valid item : list) { - field.addValidation(makeValidation(item.getValue(), null, item.isDynamic())); + field.addValidation(ImporterUtils.makeValidation(item.getValue(), null, item.isDynamic())); } } if (data.getValidations() != null) { List list = data.getValidations().getValidation(); for (com.netgrif.application.engine.importer.model.Validation item : list) { - field.addValidation(makeValidation(item.getExpression().getValue(), importer.toI18NString(item.getMessage()), item.getExpression().isDynamic())); + field.addValidation(ImporterUtils.makeValidation(item.getExpression().getValue(), importer.toI18NString(item.getMessage()), item.getExpression().isDynamic())); } } @@ -318,10 +319,6 @@ private StringCollectionField buildStringCollectionField(Data data, Importer imp return field; } - private com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation makeValidation(String rule, I18nString message, boolean dynamic) { - return dynamic ? new DynamicValidation(rule, message) : new com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation(rule, message); - } - private TaskField buildTaskField(Data data, List transitions) { TaskField field = new TaskField(); setDefaultValues(field, data, defaultValues -> { diff --git a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java index 7c6551bd47d..fd640ec643a 100644 --- a/src/main/java/com/netgrif/application/engine/importer/service/Importer.java +++ b/src/main/java/com/netgrif/application/engine/importer/service/Importer.java @@ -32,6 +32,7 @@ import com.netgrif.application.engine.petrinet.service.ArcFactory; import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService; import com.netgrif.application.engine.petrinet.service.interfaces.IProcessRoleService; +import com.netgrif.application.engine.utils.ImporterUtils; import com.netgrif.application.engine.workflow.domain.FileStorageConfiguration; import com.netgrif.application.engine.workflow.domain.ProcessResourceId; import com.netgrif.application.engine.workflow.domain.triggers.Trigger; @@ -220,7 +221,7 @@ protected Optional createPetriNet() throws MissingPetriNetMetaDataExce net.setDefaultCaseName(toI18NString(document.getCaseName())); } if (document.getTags() != null) { - net.setTags(this.buildTagsMap(document.getTags().getTag())); + net.setTags(ImporterUtils.buildTagsMap(document.getTags().getTag())); } return Optional.of(net); @@ -496,7 +497,7 @@ protected void createTransition(com.netgrif.application.engine.importer.model.Tr transition.setTitle(importTransition.getLabel() != null ? toI18NString(importTransition.getLabel()) : new I18nString("")); transition.setPosition(importTransition.getX(), importTransition.getY()); if (importTransition.getTags() != null) { - transition.setTags(this.buildTagsMap(importTransition.getTags().getTag())); + transition.setTags(ImporterUtils.buildTagsMap(importTransition.getTags().getTag())); } if (importTransition.getLayout() != null) { @@ -1322,14 +1323,4 @@ protected void setMetaData() throws MissingPetriNetMetaDataException { if (!missingMetaData.isEmpty()) throw new MissingPetriNetMetaDataException(missingMetaData); } - - protected Map buildTagsMap(List tagsList) { - Map tags = new HashMap<>(); - if (tagsList != null) { - tagsList.forEach(tag -> { - tags.put(tag.getKey(), tag.getValue()); - }); - } - return tags; - } } diff --git a/src/main/java/com/netgrif/application/engine/utils/ImporterUtils.java b/src/main/java/com/netgrif/application/engine/utils/ImporterUtils.java new file mode 100644 index 00000000000..dd08ff63add --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/utils/ImporterUtils.java @@ -0,0 +1,27 @@ +package com.netgrif.application.engine.utils; + +import com.netgrif.application.engine.importer.model.Tag; +import com.netgrif.application.engine.petrinet.domain.I18nString; +import com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.DynamicValidation; +import com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ImporterUtils { + + public static Map buildTagsMap(List tagsList) { + Map tags = new HashMap<>(); + if (tagsList != null) { + tagsList.forEach(tag -> { + tags.put(tag.getKey(), tag.getValue()); + }); + } + return tags; + } + + public static Validation makeValidation(String rule, I18nString message, boolean dynamic) { + return dynamic ? new DynamicValidation(rule, message) : new Validation(rule, message); + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java index d448dc4ddaf..d78f604a34f 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/Case.java @@ -177,8 +177,12 @@ protected Case() { } public Case(PetriNet petriNet) { + this(petriNet,new ProcessResourceId(petriNet.getObjectId())); + } + + public Case(PetriNet petriNet, ProcessResourceId _id) { this(); - this._id = new ProcessResourceId(petriNet.getObjectId()); + this._id = _id; petriNetObjectId = petriNet.getObjectId(); processIdentifier = petriNet.getIdentifier(); this.petriNet = petriNet; diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java index b3c6c16885b..c778f866b9d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java @@ -3,6 +3,7 @@ import com.netgrif.application.engine.importer.model.*; import com.netgrif.application.engine.importer.model.Properties; import com.netgrif.application.engine.petrinet.domain.I18nString; +import com.netgrif.application.engine.petrinet.domain.dataset.*; import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior; import com.netgrif.application.engine.workflow.domain.triggers.AutoTrigger; import com.netgrif.application.engine.workflow.domain.triggers.TimeTrigger; @@ -13,11 +14,16 @@ import jakarta.xml.bind.Marshaller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import java.io.OutputStream; import java.math.BigInteger; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.*; @Slf4j @@ -26,36 +32,56 @@ public class CaseExporter { @Autowired private ITaskService taskService; + @Value("${nae.schema.location}") + private String schemaLocation; + private final ObjectFactory objectFactory = new ObjectFactory(); - public void exportCases(Collection casesToExport, OutputStream outputStream) { - casesToExport.forEach(caseToExport -> exportCase(caseToExport, outputStream)); - } + private OutputStream outputStream; + private com.netgrif.application.engine.workflow.domain.Case caseToExport; + private Case xmlCase; + + // todo custom error handling? + public void exportCases(Collection casesToExport, OutputStream outputStream) throws RuntimeException { + this.outputStream = outputStream; + + Cases xmlCases = objectFactory.createCases(); + casesToExport.forEach(caseToExport -> { + this.caseToExport = caseToExport; + xmlCases.getCase().add(exportCase()); + }); - public void exportCase(com.netgrif.application.engine.workflow.domain.Case caseToExport, OutputStream outputStream) { - Case xmlCase = objectFactory.createCase(); - exportCaseMetadata(caseToExport, xmlCase); - List tasks = caseToExport.getTasks().stream() - .map(taskPair -> taskService.findOne(taskPair.getTask())) - .toList(); - exportTasks(tasks, xmlCase.getTask()); - exportDataFields(caseToExport.getDataSet(), xmlCase.getDataField()); try { - marshallCase(xmlCase, outputStream); + marshallCase(xmlCases); } catch (JAXBException e) { throw new RuntimeException(e); } } - private void exportDataFields(LinkedHashMap dataSet, List dataField) { - dataSet.forEach((key, value) -> dataField.add(exportDataField(key, value))); + private Case exportCase() { + this.xmlCase = objectFactory.createCase(); + exportCaseMetadata(caseToExport); + exportTasks(); + exportDataFields(); + return xmlCase; + } + + private void exportDataFields() { + LinkedHashMap dataSet = this.caseToExport.getDataSet(); + if (dataSet == null || dataSet.isEmpty()) { + return; + } + dataSet.forEach((key, value) -> xmlCase.getDataField().add(exportDataField(key, value))); } private DataField exportDataField(String fieldId, com.netgrif.application.engine.workflow.domain.DataField dataFieldToExport) { DataField xmlDataField = objectFactory.createDataField(); xmlDataField.setId(fieldId); - xmlDataField.setValue(exportDataFieldValue(dataFieldToExport.getValue())); - xmlDataField.setFilterMetadata(exportDataFieldValue(dataFieldToExport.getFilterMetadata())); + xmlDataField.setType(DataType.fromValue(caseToExport.getField(fieldId).getType().getName())); + xmlDataField.setValues(exportDataFieldValue(dataFieldToExport.getValue(), caseToExport.getField(fieldId).getType())); + if(this.caseToExport.getField(fieldId).getType().equals(FieldType.FILTER)) { + xmlDataField.setFilterMetadata(exportFilterMetadata(dataFieldToExport.getFilterMetadata())); + } xmlDataField.setEncryption(dataFieldToExport.getEncryption()); xmlDataField.setLastModified(exportLocalDateTime(dataFieldToExport.getLastModified())); xmlDataField.setVersion(dataFieldToExport.getVersion()); @@ -68,6 +94,60 @@ private DataField exportDataField(String fieldId, com.netgrif.application.engine return xmlDataField; } + private FilterMetadata exportFilterMetadata(Map filterMetadata) { +// todo refactor whole metadata object + if (filterMetadata == null || filterMetadata.isEmpty()) { + return null; + } + FilterMetadata xmlMetadata = objectFactory.createFilterMetadata(); + xmlMetadata.setFilterType(FilterType.fromValue(filterMetadata.get("filterType").toString())); + xmlMetadata.setPredicateMetadata(parsePredicateTreeMetadata((List>)filterMetadata.get("predicateTreeMetadata"))); + xmlMetadata.getSearchCategories().getValue().addAll((List)filterMetadata.get("searchCategories")); + xmlMetadata.setDefaultSearchCategories((Boolean) filterMetadata.get("defaultSearchCategories")); + xmlMetadata.setInheritAllowedNets((Boolean) filterMetadata.get("inheritAllowedNets")); + return xmlMetadata; + } + + private PredicateTreeMetadata parsePredicateTreeMetadata(List> predicateTreeMetadata) { + if(predicateTreeMetadata == null || predicateTreeMetadata.isEmpty()) { + return null; + } + PredicateTreeMetadata xmlPredicateTreeMetadata = objectFactory.createPredicateTreeMetadata(); + predicateTreeMetadata.forEach(list -> { + if(list == null || list.isEmpty()) { + return; + } + PredicateMetadataArray metadataArray = objectFactory.createPredicateMetadataArray(); + list.forEach(data -> { + if (data == null) { + return; + } + CategoryGeneratorMetadata metadata = objectFactory.createCategoryGeneratorMetadata(); + Map retypedData = (Map) data; + metadata.setCategory(retypedData.get("category").toString()); + metadata.setValues(exportCollectionOfStrings((Collection) retypedData.get("category"))); + metadata.setConfiguration(exportMetadataConfiguration((Map)retypedData.get("configuration"))); + metadataArray.getData().add(metadata); + }); + xmlPredicateTreeMetadata.getPredicate().add(metadataArray); + }); + return xmlPredicateTreeMetadata; + } + + private CategoryMetadataConfiguration exportMetadataConfiguration(Map configuration) { + if (configuration == null || configuration.isEmpty()) { + return null; + } + CategoryMetadataConfiguration xmlMetadata = objectFactory.createCategoryMetadataConfiguration(); + configuration.forEach((key, value) -> { + ConfigurationValue xmlValue = objectFactory.createConfigurationValue(); + xmlValue.setValue(value); + xmlValue.setId(key); + xmlMetadata.getValue().add(xmlValue); + }); + return xmlMetadata; + } + private TaskBehaviors exportTaskBehaviors(Map> taskBehavior) { if (taskBehavior == null || taskBehavior.isEmpty()) { return null; @@ -105,7 +185,7 @@ private Validations exportValidations(List { -// todo resolve other Validation object properties after 6.4.0 is merged into 7.0.0 +// todo resolve other Validation object properties after NAE-1892 is merged into 7.0.0 Validation xmlValidation = objectFactory.createValidation(); xmlValidation.setMessage(exportI18NString(validation.getValidationMessage())); xmlValidations.getValidation().add(xmlValidation); @@ -115,13 +195,14 @@ private Validations exportValidations(List exportDataRefComponents(Map dataRefComponents) { if (dataRefComponents == null || dataRefComponents.isEmpty()) { - return null; + return Collections.emptyList(); } List xmlDataRefComponents = new ArrayList<>(); dataRefComponents.forEach((taskId, component) -> { DataRefComponent xmlDataRefComponent = objectFactory.createDataRefComponent(); Component xmlComponent = exportComponent(component); xmlDataRefComponent.setTaskId(taskId); + xmlDataRefComponent.setComponent(xmlComponent); xmlDataRefComponents.add(xmlDataRefComponent); }); return xmlDataRefComponents; @@ -163,15 +244,59 @@ private Properties exportProperties(Map properties, Listvalues.getValue().add(it.toString())); + break; + case USER: + case USERLIST: + Set userFieldValues = new HashSet<>(); + if (value instanceof UserFieldValue) { + userFieldValues.add((UserFieldValue) value); + } else { + userFieldValues = ((UserListFieldValue) value).getUserValues(); + } + userFieldValues.forEach(userFieldValue -> { + values.getValue().add(userFieldValue.getId()); + }); + break; + case FILE: + case FILELIST: + Set fileFieldValues = new HashSet<>(); + if (value instanceof FileFieldValue) { + fileFieldValues.add((FileFieldValue) value); + } else { + fileFieldValues = ((FileListFieldValue) value).getNamesPaths(); + } + fileFieldValues.forEach(fieldValue -> { + values.getValue().add(fieldValue.getName().concat(":").concat(fieldValue.getPath())); + }); + break; + default: + values.getValue().add(value.toString()); + break; + } + return values; } - private void exportTasks(List tasksToExport, List xmlTasks) { - tasksToExport.forEach(taskToExport -> xmlTasks.add(exportTask(taskToExport))); + private void exportTasks() { + List tasksToExport = caseToExport.getTasks().stream() + .map(taskPair -> taskService.findOne(taskPair.getTask())) + .toList(); + tasksToExport.forEach(taskToExport -> this.xmlCase.getTask().add(exportTask(taskToExport))); } private Task exportTask(com.netgrif.application.engine.workflow.domain.Task taskToExport) { @@ -179,16 +304,16 @@ private Task exportTask(com.netgrif.application.engine.workflow.domain.Task task xmlTask.setId(taskToExport.getStringId()); xmlTask.setTransitionId(taskToExport.getTransitionId()); xmlTask.setTitle(exportI18NString(taskToExport.getTitle())); - xmlTask.setPriority(BigInteger.valueOf(taskToExport.getPriority())); + xmlTask.setPriority(exportInteger(taskToExport.getPriority())); xmlTask.setUserId(taskToExport.getUserId()); xmlTask.setStartDate(exportLocalDateTime(taskToExport.getStartDate())); xmlTask.setFinishDate(exportLocalDateTime(taskToExport.getFinishDate())); xmlTask.setFinishedBy(taskToExport.getFinishedBy()); xmlTask.setTransactionId(taskToExport.getTransactionId()); xmlTask.setIcon(taskToExport.getIcon()); - xmlTask.setAssignPolicy(AssignPolicy.fromValue(taskToExport.getAssignPolicy().toString())); - xmlTask.setDataFocusPolicy(DataFocusPolicy.fromValue(taskToExport.getDataFocusPolicy().toString())); - xmlTask.setFinishPolicy(FinishPolicy.fromValue(taskToExport.getFinishPolicy().toString())); + xmlTask.setAssignPolicy(AssignPolicy.fromValue(taskToExport.getAssignPolicy().toString().toLowerCase())); + xmlTask.setDataFocusPolicy(DataFocusPolicy.fromValue(taskToExport.getDataFocusPolicy().toString().toLowerCase())); + xmlTask.setFinishPolicy(FinishPolicy.fromValue(taskToExport.getFinishPolicy().toString().toLowerCase())); xmlTask.setTags(exportTags(taskToExport.getTags())); xmlTask.setViewRoles(exportCollectionOfStrings(taskToExport.getViewRoles())); xmlTask.setViewUserRefs(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); @@ -204,7 +329,14 @@ private Task exportTask(com.netgrif.application.engine.workflow.domain.Task task return xmlTask; } + private BigInteger exportInteger(Integer priority) { + return priority == null ? null : BigInteger.valueOf(priority); + } + private TaskTrigger exportTriggers(List triggers) { + if (triggers == null || triggers.isEmpty()) { + return null; + } TaskTrigger taskTrigger = objectFactory.createTaskTrigger(); triggers.forEach(trigger -> { TriggerWithId xmlTrigger = objectFactory.createTriggerWithId(); @@ -218,7 +350,7 @@ private TaskTrigger exportTriggers(List triggers) { triggerTypeString = TriggerType.USER; } xmlTrigger.setType(triggerTypeString); - taskTrigger.setTrigger(xmlTrigger); + taskTrigger.getTrigger().add(xmlTrigger); }); return taskTrigger; } @@ -229,40 +361,39 @@ private AssignedUserPolicies exportAssignedUserPolicy(Map assig return assignedUserPolicies; } - protected void marshallCase(com.netgrif.application.engine.importer.model.Case caseToExport, OutputStream outputStream) throws JAXBException { - JAXBContext jaxbContext = JAXBContext.newInstance(com.netgrif.application.engine.importer.model.Case.class); + protected void marshallCase(com.netgrif.application.engine.importer.model.Cases caseToExport) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(com.netgrif.application.engine.importer.model.Cases.class); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); -// todo extract to property? - marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "https://petriflow.com/petriflow.schema.xsd"); - marshaller.marshal(caseToExport, outputStream); + marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, schemaLocation); + marshaller.marshal(caseToExport, this.outputStream); } - private void exportCaseMetadata(com.netgrif.application.engine.workflow.domain.Case caseToExport, Case xmlCase) { - xmlCase.setId(caseToExport.getStringId()); + private void exportCaseMetadata(com.netgrif.application.engine.workflow.domain.Case caseToExport) { + this.xmlCase.setId(caseToExport.getStringId()); // todo should the whole object be exported? is id specific enough? should email be exported instead of id? - xmlCase.setAuthor(caseToExport.getAuthor().getId()); - xmlCase.setColor(caseToExport.getColor()); - xmlCase.setProcessVersion(caseToExport.getPetriNet().getVersion().toString()); - xmlCase.setProcessIdentifier(xmlCase.getProcessIdentifier()); - xmlCase.setVisualId(xmlCase.getVisualId()); - xmlCase.setUriNodeId(xmlCase.getUriNodeId()); - xmlCase.setTags(exportTags(caseToExport.getTags())); - xmlCase.setTitle(caseToExport.getTitle()); - xmlCase.setCreationDate(exportLocalDateTime(caseToExport.getCreationDate())); - xmlCase.setLastModified(exportLocalDateTime(caseToExport.getLastModified())); - xmlCase.setEnabledRoles(exportCollectionOfStrings(caseToExport.getEnabledRoles())); - xmlCase.setViewRoles(exportCollectionOfStrings(caseToExport.getViewRoles())); - xmlCase.setViewUserRefs(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - xmlCase.setViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - xmlCase.setNegativeViewRoles(exportCollectionOfStrings(caseToExport.getNegativeViewRoles())); - xmlCase.setNegativeViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - xmlCase.setImmediateDataFields(exportCollectionOfStrings(caseToExport.getImmediateDataFields())); - xmlCase.setActivePlaces(exportMapXsdType(caseToExport.getActivePlaces())); - xmlCase.setConsumedTokens(exportMapXsdType(caseToExport.getConsumedTokens())); - xmlCase.setPermissions(exportPermissions(caseToExport.getPermissions())); - xmlCase.setUserRefs(exportPermissions(caseToExport.getUserRefs())); - xmlCase.setUsers(exportPermissions(caseToExport.getUsers())); + this.xmlCase.setAuthor(caseToExport.getAuthor().getId()); + this.xmlCase.setColor(caseToExport.getColor()); + this.xmlCase.setProcessVersion(caseToExport.getPetriNet().getVersion().toString()); + this.xmlCase.setProcessIdentifier(caseToExport.getProcessIdentifier()); + this.xmlCase.setVisualId(caseToExport.getVisualId()); + this.xmlCase.setUriNodeId(caseToExport.getUriNodeId()); + this.xmlCase.setTags(exportTags(caseToExport.getTags())); + this.xmlCase.setTitle(caseToExport.getTitle()); + this.xmlCase.setCreationDate(exportLocalDateTime(caseToExport.getCreationDate())); + this.xmlCase.setLastModified(exportLocalDateTime(caseToExport.getLastModified())); + this.xmlCase.setEnabledRoles(exportCollectionOfStrings(caseToExport.getEnabledRoles())); + this.xmlCase.setViewRoles(exportCollectionOfStrings(caseToExport.getViewRoles())); + this.xmlCase.setViewUserRefs(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + this.xmlCase.setViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + this.xmlCase.setNegativeViewRoles(exportCollectionOfStrings(caseToExport.getNegativeViewRoles())); + this.xmlCase.setNegativeViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); + this.xmlCase.setImmediateDataFields(exportCollectionOfStrings(caseToExport.getImmediateDataFields())); + this.xmlCase.setActivePlaces(exportMapXsdType(caseToExport.getActivePlaces())); + this.xmlCase.setConsumedTokens(exportMapXsdType(caseToExport.getConsumedTokens())); + this.xmlCase.setPermissions(exportPermissions(caseToExport.getPermissions())); + this.xmlCase.setUserRefs(exportPermissions(caseToExport.getUserRefs())); + this.xmlCase.setUsers(exportPermissions(caseToExport.getUsers())); } private Tags exportTags(Map tags) { @@ -280,28 +411,39 @@ private Tags exportTags(Map tags) { } private I18NStringType exportI18NString(I18nString i18nString) { + if (i18nString == null) { + return null; + } I18NStringType i18NStringType = objectFactory.createI18NStringType(); i18NStringType.setName(i18nString.getKey()); i18NStringType.setValue(i18nString.getDefaultValue()); +// todo export translations too probably return i18NStringType; } private String exportLocalDateTime(LocalDateTime toExport) { - return toExport == null ? null : toExport.format(DateTimeFormatter.ISO_ZONED_DATE_TIME); +// todo LocalDate and DateTime do not store information about Zone/Offset, Date and DateTime fields needs to be refactored to store values as either Zoned or Offset dateTime + return toExport == null ? null : toExport.atZone(ZoneId.systemDefault()).truncatedTo(ChronoUnit.SECONDS).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); } private StringCollection exportCollectionOfStrings(Collection collection) { + if (collection == null || collection.isEmpty()) { + return null; + } StringCollection stringCollection = objectFactory.createStringCollection(); stringCollection.getValue().addAll(collection); return stringCollection; } private MapXsdType exportMapXsdType(Map map) { + if (map == null || map.isEmpty()) { + return null; + } MapXsdType xsdType = objectFactory.createMapXsdType(); map.forEach((key, value) -> { IntegerMapEntry mapEntry = objectFactory.createIntegerMapEntry(); mapEntry.setKey(key); - mapEntry.setValue(BigInteger.valueOf(value)); + mapEntry.setValue(exportInteger(value)); xsdType.getEntry().add(mapEntry); }); return xsdType; diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java new file mode 100644 index 00000000000..4f2cb83559a --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java @@ -0,0 +1,91 @@ +package com.netgrif.application.engine.workflow.service; + +import com.netgrif.application.engine.archive.interfaces.IArchiveService; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.FileStorageConfiguration; +import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.lingala.zip4j.ZipFile; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CaseImportExportService implements ICaseExportImportService { + + private final ObjectFactory caseImporterObjectFactory; + private final ObjectFactory caseExporterObjectFactory; + private final IArchiveService archiveService; + private final FileStorageConfiguration fileStorageConfiguration; + private final IWorkflowService workflowService; + + protected CaseImporter getCaseImporter() { + return caseImporterObjectFactory.getObject(); + } + + protected CaseExporter getCaseExporter() { + return caseExporterObjectFactory.getObject(); + } + + @Override + public void findAndExportCases(Set caseIdsToExport, OutputStream exportFile) { + this.exportCases(caseIdsToExport.stream().map(caseId -> workflowService.findOne(caseId)).collect(Collectors.toSet()), exportFile); + } + + @Override + public void exportCases(Set casesToExport, OutputStream exportFile) { + this.exportCasesToFile(casesToExport, exportFile); + } + + @Override + public Set getFilesOfCases(Set casesToExport) { + return Set.of(); + } + + @Override + public ZipFile zipExportFileAndCaseFiles(OutputStream exportFile, Set caseFiles) { + return null; + } + + + @Override + public File exportCasesWithFiles(Collection casesToExport, File exportFile) { + try (FileOutputStream fos = new FileOutputStream(exportFile)) { + this.exportCasesToFile(casesToExport, fos); + String[] filePathsToZip = (String[]) casesToExport.stream() + .map(exportCase -> fileStorageConfiguration.getStoragePath() + "/" + exportCase.getStringId()) + .filter(filePath -> Files.exists(Paths.get(filePath))) + .toArray(); +// todo archive name/path + archiveService.pack(fileStorageConfiguration.getStoragePath() + "/exports", filePathsToZip); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + + @Override + public List importCases(InputStream inputStream) { + return caseImporterObjectFactory.getObject().importCases(inputStream); + } + + private void exportCasesToFile(Collection casesToExport, OutputStream exportFile) { + CaseExporter caseExporter = getCaseExporter(); + try { + caseExporter.exportCases(casesToExport, exportFile); + } catch (RuntimeException e) { + log.error("Error exporting cases", e); + } + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java new file mode 100644 index 00000000000..7330ffbd616 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java @@ -0,0 +1,446 @@ +package com.netgrif.application.engine.workflow.service; + +import com.netgrif.application.engine.auth.domain.IUser; +import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.importer.model.*; +import com.netgrif.application.engine.importer.service.ComponentFactory; +import com.netgrif.application.engine.importer.service.TriggerFactory; +import com.netgrif.application.engine.petrinet.domain.Component; +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.domain.dataset.UserFieldValue; +import com.netgrif.application.engine.petrinet.domain.dataset.UserListFieldValue; +import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior; +import com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation; +import com.netgrif.application.engine.petrinet.domain.policies.AssignPolicy; +import com.netgrif.application.engine.petrinet.domain.policies.DataFocusPolicy; +import com.netgrif.application.engine.petrinet.domain.policies.FinishPolicy; +import com.netgrif.application.engine.petrinet.domain.version.Version; +import com.netgrif.application.engine.petrinet.service.PetriNetService; +import com.netgrif.application.engine.utils.ImporterUtils; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.DataField; +import com.netgrif.application.engine.workflow.domain.ProcessResourceId; +import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.domain.triggers.Trigger; +import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class CaseImporter { + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private PetriNetService petriNetService; + + @Autowired + private IUserService userService; + + @Autowired + protected ComponentFactory componentFactory; + + @Autowired + protected TriggerFactory triggerFactory; + + @Autowired + protected ITaskService taskService; + + private Cases xmlCases; + private Case importedCase; + private com.netgrif.application.engine.importer.model.Case xmlCase; + + + @Transactional + public List importCases(InputStream xml) { + List importedCases = new ArrayList<>(); + try { + unmarshallXml(xml); + } catch (JAXBException e) { + log.error("Error unmarshalling input xml file: ", e); + return Collections.emptyList(); + } + for (com.netgrif.application.engine.importer.model.Case xmlCase : xmlCases.getCase()) { + importCase(xmlCase); + if (importedCase == null) { + continue; + } + importedCases.add(importedCase); + } + return importedCases; + } + + @Transactional + protected Case importCase(com.netgrif.application.engine.importer.model.Case xmlCase) { + this.xmlCase = xmlCase; + Version version = new Version(); + PetriNet model = petriNetService.getPetriNet(xmlCase.getProcessIdentifier(), version); + if (model == null) { +// todo throw error + log.error("Petri net with identifier [" + xmlCase.getProcessIdentifier() + "] not found, skipping case import"); + return null; + } + String importedCaseStringId = xmlCase.getId().split("-")[1].trim(); + try { + workflowService.findOne(importedCaseStringId); + this.importedCase = new Case(model, new ProcessResourceId(model.getStringId(), importedCaseStringId.split("-")[1].trim())); + } catch (IllegalArgumentException e) { + log.warn("Case with id [{}] already exists, new id will be generated for imported case", xmlCase.getId()); + this.importedCase = new Case(model); + } + importCaseMetadata(); + importDataSet(); + importTasks(); + return importedCase; + } + + @Transactional + protected void importTasks() { + List importedTasks = new ArrayList<>(); + if (xmlCase.getTask() == null) { + return; + } + this.xmlCase.getTask().forEach(task -> { + Task importedTask = Task.with() + .caseId(this.importedCase.getStringId()) + .transitionId(task.getTransitionId()) + .title(parseXmlI18nString(task.getTitle())) + .priority(task.getPriority() != null ? task.getPriority().intValue() : null) + .userId(task.getUserId()) + .startDate(parseDateTimeFromXml(task.getStartDate())) + .finishDate(parseDateTimeFromXml(task.getFinishDate())) + .finishedBy(task.getFinishedBy()) + .transactionId(task.getTransactionId()) + .icon(task.getIcon()) + .assignPolicy(AssignPolicy.valueOf(task.getAssignPolicy().value().toUpperCase())) + .dataFocusPolicy(DataFocusPolicy.valueOf(task.getDataFocusPolicy().value().toUpperCase())) + .finishPolicy(FinishPolicy.valueOf(task.getFinishPolicy().value().toUpperCase())) + .tags(task.getTags() != null ? ImporterUtils.buildTagsMap(task.getTags().getTag()) : new HashMap<>()) + .viewRoles(parseStringCollection(task.getViewRoles())) + .viewUserRefs(parseStringCollection(task.getViewUserRefs())) + .viewUsers(parseStringCollection(task.getViewUsers())) + .negativeViewRoles(parseStringCollection(task.getNegativeViewRoles())) + .negativeViewUsers(parseStringCollection(task.getNegativeViewUsers())) + .immediateDataFields(new LinkedHashSet<>(parseStringCollection(task.getImmediateDataFields()))) + .roles(parsePermissionMap(task.getRole())).userRefs(parsePermissionMap(task.getUserRef())) + .users(parsePermissionMap(task.getUser())) + .assignedUserPolicy(task.getAssignedUserPolicies().getAssignedUserPolicy().stream().collect(Collectors.toMap(BooleanMapEntry::getKey, BooleanMapEntry::isValue))) + .triggers(parseXmlTriggers(task.getTriggers())) + .build(); + importedTasks.add(importedTask); + importedCase.addTask(importedTask); + }); + taskService.save(importedTasks); + } + + private List parseXmlTriggers(TaskTrigger triggers) { + List triggerList = new ArrayList<>(); + if (triggers == null) { + return triggerList; + } + triggers.getTrigger().forEach(trigger -> { + Trigger taskTrigger = triggerFactory.buildTrigger(trigger); + taskTrigger.set_id(new ObjectId(trigger.getId())); + triggerList.add(taskTrigger); + }); + return triggerList; + } + + @Transactional + protected void importDataSet() { + xmlCase.getDataField().forEach(field -> { + DataField dataField = new DataField(); + dataField.setEncryption(field.getEncryption()); + dataField.setLastModified(parseDateTimeFromXml(field.getLastModified())); + dataField.setVersion(field.getVersion()); + if(field.getType() == DataType.FILTER) { + dataField.setFilterMetadata(parseFilterMetadata(field.getFilterMetadata())); + } + dataField.setValue(parseXmlValue(field.getValues(), field.getType())); + dataField.setComponent(parseXmlComponent(field.getComponent())); + field.getDataRefComponent().stream() + .filter(dataRefComponent -> dataRefComponent.getComponent() != null) + .forEach(dataRefComponent -> dataField.getDataRefComponents().put(dataRefComponent.getTaskId(), parseXmlComponent(dataRefComponent.getComponent()))); + dataField.setValidations(parseXmlValidations(field.getValidations())); + dataField.setOptions(parseXmlOptions(field.getOptions())); + dataField.setBehavior(parseXmlBehaviors(field.getBehaviors())); + importedCase.getDataSet().put(field.getId(), dataField); + }); + } + + private Map parseFilterMetadata(FilterMetadata filterMetadata) { + Map filterMetadataMap = new HashMap<>(); + if (filterMetadata == null) { + return filterMetadataMap; + } + filterMetadataMap.put("filterType", filterMetadata.getFilterType().toString()); + filterMetadataMap.put("predicateMetadata", parsePredicateMetadata(filterMetadata.getPredicateMetadata())); + filterMetadataMap.put("searchCategories", filterMetadata.getSearchCategories()); + filterMetadataMap.put("defaultSearchCategories", filterMetadata.isDefaultSearchCategories()); + filterMetadataMap.put("inheritAllowedNets", filterMetadata.isInheritAllowedNets()); + return filterMetadataMap; + } + + private Object parsePredicateMetadata(PredicateTreeMetadata predicateMetadata) { + if(predicateMetadata == null || predicateMetadata.getPredicate() == null) { + return null; + } + List> values = new ArrayList<>(); + predicateMetadata.getPredicate().forEach(predicate -> { + if(predicate == null) { + return; + } + List value = new ArrayList<>(); + predicate.getData().forEach(data -> { + if(data == null) { + return; + } + Map dataMap = new HashMap<>(); + dataMap.put("category", data.getCategory()); + dataMap.put("values", parseStringCollection(data.getValues())); + dataMap.put("configuration", parseConfiguration(data.getConfiguration())); + value.add(dataMap); + }); + values.add(value); + }); + return values; + } + + private Object parseConfiguration(CategoryMetadataConfiguration configuration) { + if(configuration == null) { + return null; + } + Map configurationMap = new HashMap<>(); + configuration.getValue().forEach(data -> { + if(data == null) { + return; + } + configurationMap.put(data.getId(), data.getValue()); + }); + return configurationMap; + } + + private Object parseXmlValue(StringCollection value, DataType dataType) { + if (value == null || value.getValue() == null || value.getValue().isEmpty()) { + return null; + } + Object parsedValue; + switch (dataType) { + case DATE: + case DATE_TIME: + parsedValue = parseDateTimeFromXml(value.getValue().getFirst()); + if (dataType == DataType.DATE) { + parsedValue = ((LocalDateTime) parsedValue).toLocalDate(); + } + break; + case STRING_COLLECTION: + case CASE_REF: + case TASK_REF: + parsedValue = parseStringCollection(value); + break; + case NUMBER: + parsedValue = Double.parseDouble(value.getValue().getFirst()); + break; + case BOOLEAN: + parsedValue = Boolean.parseBoolean(value.getValue().getFirst()); + break; + case MULTICHOICE: + case MULTICHOICE_MAP: + parsedValue = parseStringCollection(value, new LinkedHashSet<>()); + if (dataType == DataType.MULTICHOICE) { + parsedValue = ((Set) parsedValue).stream().map(I18nString::new).collect(Collectors.toCollection(LinkedHashSet::new)); + } + break; + case USER: + parsedValue = parseUserFieldValue(value.getValue().getFirst()); + break; + case USER_LIST: + parsedValue = new UserListFieldValue(value.getValue().stream().map(this::parseUserFieldValue).collect(Collectors.toList())); + break; + case FILE: +// todo path check/replace if new id for case was generated + parsedValue = FileFieldValue.fromString(value.getValue().getFirst()); + break; + case FILE_LIST: + parsedValue = FileListFieldValue.fromString(value.getValue().getFirst()); + break; + case ENUMERATION: + case I_18_N: + parsedValue = new I18nString(value.getValue().getFirst()); + break; + case BUTTON: + parsedValue = Integer.parseInt(value.getValue().getFirst()); + break; + default: + parsedValue = value.getValue().getFirst(); + break; + } + return parsedValue; + } + + private UserFieldValue parseUserFieldValue(String xmlValue) { + IUser user; + try { + user = userService.resolveById(xmlValue, true); + return new UserFieldValue(user); + } catch (IllegalArgumentException e) { + log.error("User with id [" + xmlValue + "] not found, setting empty value"); + return new UserFieldValue(); + } + } + + private Map> parseXmlBehaviors(TaskBehaviors behaviors) { + Map> behaviorMap = new HashMap<>(); + if (behaviors == null || behaviors.getTaskBehavior() == null || behaviors.getTaskBehavior().isEmpty()) { + return behaviorMap; + } + behaviors.getTaskBehavior().forEach(taskBehavior -> { + Set behaviorSet = new HashSet<>(); + taskBehavior.getBehavior().forEach(behavior -> { + behaviorSet.add(FieldBehavior.fromString(behavior)); + }); + behaviorMap.put(taskBehavior.getTaskId(), behaviorSet); + }); + return behaviorMap; + } + + private Map parseXmlOptions(Options options) { + Map optionsMap = new HashMap<>(); + if (options == null || options.getOption() == null) { + return optionsMap; + } + options.getOption().forEach(option -> { + optionsMap.put(option.getKey(), parseXmlI18nString(option)); + }); + return optionsMap; + } + + private List parseXmlValidations(Validations validations) { + List parsedValidations = new ArrayList<>(); + if (validations == null) { + return parsedValidations; + } + validations.getValidation().forEach(validation -> { +// todo problem with i18n translations, petriNet does not store information about translations on their own, they get resolved during net import and are stored in i18n objects +// todo export i18ns with their translations? refactor petriNet? + parsedValidations.add(ImporterUtils.makeValidation(validation.getExpression().getValue(), parseXmlI18nString(validation.getMessage()), validation.getExpression().isDynamic())); + }); + return parsedValidations; + } + + private I18nString parseXmlI18nString(I18NStringType xmlI18nString) { + if (xmlI18nString == null) { + return null; + } + I18nString parsedI18n = new I18nString(); + parsedI18n.setKey(xmlI18nString.getName()); + parsedI18n.setDefaultValue(xmlI18nString.getValue()); + return parsedI18n; + } + + private Component parseXmlComponent(com.netgrif.application.engine.importer.model.Component component) { + return component == null ? null : componentFactory.buildComponent(component); + } + + @Transactional + protected void importCaseMetadata() { +// todo id and visualId cannot be set, both are generated in constructor + IUser user; + try { + user = userService.findById(xmlCase.getAuthor(), true); + } catch (IllegalArgumentException e) { + log.warn("Author of case to be imported not found, setting technical user as author"); + user = userService.getSystem(); + } + importedCase.setAuthor(user.transformToAuthor()); + if (xmlCase.getTags() != null) { + importedCase.setTags(ImporterUtils.buildTagsMap(xmlCase.getTags().getTag())); + } +// todo date validate + importedCase.setUriNodeId(xmlCase.getUriNodeId()); + importedCase.setCreationDate(parseDateTimeFromXml(xmlCase.getCreationDate())); + importedCase.setLastModified(parseDateTimeFromXml(xmlCase.getLastModified())); + importedCase.setEnabledRoles(parseStringCollection(xmlCase.getEnabledRoles(), new HashSet<>())); + importedCase.setViewRoles(parseStringCollection(xmlCase.getViewRoles())); + importedCase.setViewUserRefs(parseStringCollection(xmlCase.getViewUserRefs())); + importedCase.setViewUsers(parseStringCollection(xmlCase.getViewUsers())); + importedCase.setNegativeViewRoles(parseStringCollection(xmlCase.getNegativeViewRoles())); + importedCase.setNegativeViewUsers(parseStringCollection(xmlCase.getNegativeViewUsers())); + importedCase.setImmediateDataFields(parseStringCollection(xmlCase.getImmediateDataFields(), new LinkedHashSet<>())); + importedCase.setConsumedTokens(parseMapXsdType(xmlCase.getConsumedTokens())); + importedCase.setActivePlaces(parseMapXsdType(xmlCase.getActivePlaces())); + importedCase.setPermissions(parsePermissionMap(xmlCase.getPermissions())); + importedCase.setUserRefs(parsePermissionMap(xmlCase.getUserRefs())); + importedCase.setUsers(parsePermissionMap(xmlCase.getUsers())); + } + + private List parseStringCollection(StringCollection xmlCollection) { + return parseStringCollection(xmlCollection, new ArrayList<>()); + } + + private T parseStringCollection(StringCollection xmlCollection, T collection) { + if (xmlCollection != null) { + collection.addAll(xmlCollection.getValue()); + } + return collection; + } + + private Map> parsePermissionMap(PermissionMap permissions) { + Map> permissionMap = new HashMap<>(); + if (permissions == null) { + return permissionMap; + } + permissions.getEntry().forEach(entry -> { + Map permission = new HashMap<>(); + entry.getPermission().forEach(xmlPermission -> permission.put(xmlPermission.getKey(), xmlPermission.isValue())); + permissionMap.put(entry.getId(), permission); + }); + return permissionMap; + } + + private Map parseMapXsdType(MapXsdType consumedTokens) { + Map map = new HashMap<>(); + if (consumedTokens == null) { + return map; + } + consumedTokens.getEntry().forEach(entry -> { + map.put(entry.getKey(), entry.getValue().intValue()); + }); + return map; + } + + @Transactional + protected void unmarshallXml(InputStream xml) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(Cases.class); + + Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); + xmlCases = (Cases) jaxbUnmarshaller.unmarshal(xml); + } + + private Version parseProcessVersionFromXml(String xmlVersion) { + String[] versionParts = xmlVersion.split("\\."); +// todo version validation + return new Version(Integer.parseInt(versionParts[0].trim()), Integer.parseInt(versionParts[1].trim()), Integer.parseInt(versionParts[2].trim())); + } + + private LocalDateTime parseDateTimeFromXml(String xmlDateTimeString) { + return xmlDateTimeString == null ? null : LocalDateTime.parse(xmlDateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java new file mode 100644 index 00000000000..9f75bcef7b3 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java @@ -0,0 +1,26 @@ +package com.netgrif.application.engine.workflow.service.interfaces; + +import com.netgrif.application.engine.workflow.domain.Case; +import net.lingala.zip4j.ZipFile; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public interface ICaseExportImportService { + + void findAndExportCases(Set caseIdsToExport, OutputStream exportFile); + + void exportCases(Set casesToExport, OutputStream exportFile); + + Set getFilesOfCases(Set casesToExport); + + ZipFile zipExportFileAndCaseFiles(OutputStream exportFile, Set caseFiles); + + File exportCasesWithFiles(Collection casesToExport, File exportFile); + + List importCases(InputStream importFile); +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java new file mode 100644 index 00000000000..f144f0eabd2 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java @@ -0,0 +1,59 @@ +package com.netgrif.application.engine.workflow.web; + +import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Set; + +@Slf4j +@RestController() +@RequestMapping("/api/case") +public class CaseImportExportController { + + @Autowired + private ICaseExportImportService caseExportImportService; + + @Operation(summary = "Download xml file containing exported case data", security = {@SecurityRequirement(name = "BasicAuth")}) + @GetMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportCase(@RequestParam("caseId") String caseId) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + caseExportImportService.findAndExportCases(Set.of(caseId), outputStream); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + caseId + ".xml\""); +//todo delete file after after returning + return ResponseEntity + .ok() + .headers(headers) + .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); + } + + @Operation(summary = "Import case from xml file file", security = {@SecurityRequirement(name = "BasicAuth")}) + @PostMapping(value = "/import", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity importCase(@RequestPart(value = "file") MultipartFile multipartFile) { + try { + caseExportImportService.importCases(multipartFile.getInputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return ResponseEntity + .ok() + .headers(headers) + .body("Import successful"); + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 9f45be6d199..d1558757d15 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -50,4 +50,7 @@ management.health.mail.enabled=false nae.storage.minio.enabled=true nae.storage.minio.hosts.host_1.host=http://127.0.0.1:9000 nae.storage.minio.hosts.host_1.user=root -nae.storage.minio.hosts.host_1.password=password \ No newline at end of file +nae.storage.minio.hosts.host_1.password=password + +#Schema +nae.schema.location=${SCHEMA_LOCATION:https://petriflow.com/petriflow.schema.xsd} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 526e4683e57..20b5444e86f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -197,3 +197,6 @@ management.endpoint.health.probes.enabled=true logging.file.path=log management.info.build.enabled=false management.info.env.enabled=true + +#Schema +nae.schema.location=https://petriflow.com/petriflow.schema.xsd diff --git a/src/main/resources/petriNets/petriflow_schema.xsd b/src/main/resources/petriNets/petriflow_schema.xsd index 28e88a26d6f..0e346d4ac9f 100644 --- a/src/main/resources/petriNets/petriflow_schema.xsd +++ b/src/main/resources/petriNets/petriflow_schema.xsd @@ -1,8 +1,1012 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This attribute is deprecated, use ... instead + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java b/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java new file mode 100644 index 00000000000..53ec066cc3a --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java @@ -0,0 +1,89 @@ +package com.netgrif.application.engine.workflow; + +import com.netgrif.application.engine.TestHelper; +import com.netgrif.application.engine.petrinet.domain.PetriNet; +import com.netgrif.application.engine.petrinet.domain.VersionType; +import com.netgrif.application.engine.petrinet.domain.throwable.MissingPetriNetMetaDataException; +import com.netgrif.application.engine.petrinet.service.PetriNetService; +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.service.CaseExporter; +import com.netgrif.application.engine.workflow.service.CaseImporter; +import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; +import lombok.extern.slf4j.Slf4j; +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.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +@Slf4j +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class CaseExporterTest { + + private final String testNetFileName = "all_data.xml"; + private final String testNetIdentifier = "all_data"; + private final String outputFileLocation = "src/test/resources/"; + private final String outputFileName = "case_export_test.xml"; + + @Autowired + private SuperCreatorRunner superCreator; + + @Autowired + private CaseExporter caseExporter; + + @Autowired + private PetriNetService petriNetService; + + @Autowired + private IWorkflowService workflowService; + + @Autowired + private TestHelper testHelper; + + @Autowired + private CaseImporter caseImporter; + + private PetriNet petriNet; + + @BeforeEach + public void before() { + testHelper.truncateDbs(); + try (FileInputStream fis = new FileInputStream("src/test/resources/" + testNetFileName)) { + petriNet = petriNetService.importPetriNet(fis, VersionType.MAJOR, superCreator.getLoggedSuper()).getNet(); + } catch (MissingPetriNetMetaDataException | IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void exportCase() { + Case toExport = workflowService.createCaseByIdentifier(testNetIdentifier, "export case", "", superCreator.getLoggedSuper()).getCase(); + try (FileOutputStream fos = new FileOutputStream(outputFileLocation + outputFileName)) { + caseExporter.exportCases(List.of(toExport), fos); + } catch (IOException e) { + log.error("IO exception occured", e); + } + workflowService.deleteCase(toExport); + } + + @Test + public void importCase() { + try (FileInputStream fis = new FileInputStream(outputFileLocation + outputFileName)) { + List importedCases = caseImporter.importCases(fis); + assert importedCases != null && !importedCases.isEmpty(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file From 799c8b8da8e604758ab4bdd3e9fa7c939904ba08 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Tue, 11 Mar 2025 13:24:36 +0100 Subject: [PATCH 3/9] [NAE-1843] Import/Export services - service name typo fix --- ...eImportExportService.java => CaseExportImportService.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/main/java/com/netgrif/application/engine/workflow/service/{CaseImportExportService.java => CaseExportImportService.java} (94%) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java similarity index 94% rename from src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java rename to src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java index 4f2cb83559a..abe7e1e0d5f 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java @@ -22,7 +22,7 @@ @Service @Slf4j @RequiredArgsConstructor -public class CaseImportExportService implements ICaseExportImportService { +public class CaseExportImportService implements ICaseExportImportService { private final ObjectFactory caseImporterObjectFactory; private final ObjectFactory caseExporterObjectFactory; @@ -40,7 +40,7 @@ protected CaseExporter getCaseExporter() { @Override public void findAndExportCases(Set caseIdsToExport, OutputStream exportFile) { - this.exportCases(caseIdsToExport.stream().map(caseId -> workflowService.findOne(caseId)).collect(Collectors.toSet()), exportFile); + this.exportCases(caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()), exportFile); } @Override From e560c5b9c62e1780efb7393064ba62b97fdc5083 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 24 Mar 2025 13:13:36 +0100 Subject: [PATCH 4/9] [NAE-1843] Import/Export services - changes to case import and export functionality - translations are now exported - import and export of roles removed - implementation of zip service added - work in progress on tests --- .../engine/archive/ZipService.java | 126 +++++++++- .../archive/interfaces/IArchiveService.java | 17 +- .../workflow/domain/CaseExportFiles.java | 30 +++ .../service/CaseExportImportService.java | 115 ++++++--- .../engine/workflow/service/CaseExporter.java | 205 ++++++++-------- .../engine/workflow/service/CaseImporter.java | 222 ++++++++++-------- .../engine/workflow/service/TaskService.java | 2 +- .../interfaces/ICaseExportImportService.java | 17 +- .../service/interfaces/ITaskService.java | 3 + .../web/CaseImportExportController.java | 68 +++++- .../resources/petriNets/petriflow_schema.xsd | 3 + 11 files changed, 560 insertions(+), 248 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java diff --git a/src/main/java/com/netgrif/application/engine/archive/ZipService.java b/src/main/java/com/netgrif/application/engine/archive/ZipService.java index 28d2ef5ff42..7fb855507e2 100644 --- a/src/main/java/com/netgrif/application/engine/archive/ZipService.java +++ b/src/main/java/com/netgrif/application/engine/archive/ZipService.java @@ -1,31 +1,147 @@ package com.netgrif.application.engine.archive; import com.netgrif.application.engine.archive.interfaces.IArchiveService; +import com.netgrif.application.engine.files.IStorageResolverService; +import com.netgrif.application.engine.files.interfaces.IStorageService; +import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; +import com.netgrif.application.engine.workflow.domain.CaseExportFiles; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.springframework.stereotype.Service; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Set; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; @Slf4j @Service +@RequiredArgsConstructor public class ZipService implements IArchiveService { + private final IStorageResolverService storageResolverService; + + @Override + public void pack(String archivePath, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException { + FileOutputStream fos = new FileOutputStream(archivePath); + this.pack(fos, caseExportFiles, additionalFiles); + fos.close(); + } + + @Override + public void pack(OutputStream archiveStream, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException { + ZipOutputStream zipStream = new ZipOutputStream(archiveStream); + for (String caseId : caseExportFiles.getCaseIds()) { + for (ImmutablePair, Set> fields : caseExportFiles.getFieldsOfCase(caseId)) { + StorageField storageField = fields.left; + IStorageService storageService = storageResolverService.resolve(storageField.getStorageType()); + for (String fileName : fields.right) { + String filePath = storageService.getPath(caseId, storageField.getStringId(), fileName); + InputStream fis = storageService.get(storageField, filePath); + String newFileName = caseId.concat(File.separator).concat(storageField.getStringId()).concat(File.separator).concat(fileName); + createAndWriteZipEntry(newFileName, zipStream, fis); + fis.close(); + } + } + } + for (String filePath : additionalFiles) { + InputStream fis = new FileInputStream(filePath); + createAndWriteZipEntry(Paths.get(filePath).getFileName().toString(), zipStream, fis); + fis.close(); + } + zipStream.close(); + } + @Override - public void pack(String archivePath, String... filePaths) throws FileNotFoundException { - this.pack(new FileOutputStream(archivePath), filePaths); + public OutputStream createArchive(CaseExportFiles caseExportFiles) throws IOException { + return createArchive(Files.createTempFile(UUID.randomUUID().toString(), ".zip").toString(), caseExportFiles); } @Override - public void pack(OutputStream archiveStream, String... filePaths) { + public OutputStream createArchive(String archivePath, CaseExportFiles caseExportFiles) throws IOException { + File zipFile = new File(archivePath); + return zipFiles(zipFile, caseExportFiles); + } + + private OutputStream zipFiles(File zipFile, CaseExportFiles caseExportFiles) throws IOException { + FileOutputStream fos = new FileOutputStream(zipFile); + this.pack(fos, caseExportFiles); + return fos; } @Override - public void unpack(String archivePath, String outputPath) { + public void append(OutputStream archiveStream, String... filePaths) throws IOException { +// todo implement + } + private void createAndWriteZipEntry(String fileName, ZipOutputStream zipStream, InputStream fis) throws IOException { +// source https://www.baeldung.com/java-compress-and-uncompress + ZipEntry zipEntry = new ZipEntry(fileName); + zipStream.putNextEntry(zipEntry); + byte[] bytes = new byte[1024]; + int length; + while ((length = fis.read(bytes)) >= 0) { + zipStream.write(bytes, 0, length); + } } @Override - public void unpack(InputStream archiveStream, String outputPath) { + public String unpack(String archivePath, String outputPath) throws IOException { + return this.unpack(new FileInputStream(archivePath), outputPath); + } + + @Override + public String unpack(InputStream archiveStream, String outputPath) throws IOException { +// source: https://www.baeldung.com/java-compress-and-uncompress + File destDir = new File(outputPath); + + byte[] buffer = new byte[1024]; + ZipInputStream zis = new ZipInputStream(archiveStream); + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + File newFile = newFile(destDir, zipEntry); + if (zipEntry.isDirectory()) { + if (!newFile.isDirectory() && !newFile.mkdirs()) { + throw new IOException("Failed to create directory " + newFile); + } + } else { + // fix for Windows-created archives + File parent = newFile.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + throw new IOException("Failed to create directory " + parent); + } + + // write file content + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + } + zipEntry = zis.getNextEntry(); + } + + zis.closeEntry(); + zis.close(); + return destDir.getPath(); + } + + private File newFile(File destinationDir, ZipEntry zipEntry) throws IOException { + File destFile = new File(destinationDir, zipEntry.getName()); + + String destDirPath = destinationDir.getCanonicalPath(); + String destFilePath = destFile.getCanonicalPath(); + + if (!destFilePath.startsWith(destDirPath + File.separator)) { + throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); + } + return destFile; } } diff --git a/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java b/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java index 506d02c3860..8ab4d1e3d2a 100644 --- a/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java +++ b/src/main/java/com/netgrif/application/engine/archive/interfaces/IArchiveService.java @@ -1,16 +1,25 @@ package com.netgrif.application.engine.archive.interfaces; +import com.netgrif.application.engine.workflow.domain.CaseExportFiles; + import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public interface IArchiveService { - void pack(String archivePath, String... filePaths) throws FileNotFoundException; + void pack(String archivePath, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException; + + void pack(OutputStream archiveStream, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException; + + OutputStream createArchive(CaseExportFiles caseExportFiles) throws IOException; + + OutputStream createArchive(String archivePath, CaseExportFiles caseExportFiles) throws IOException; - void pack(OutputStream archiveStream, String... filePaths); + void append(OutputStream archiveStream, String... filePaths) throws IOException; - void unpack(String archivePath, String outputPath); + String unpack(String archivePath, String outputPath) throws IOException; - void unpack(InputStream archiveStream, String outputPath); + String unpack(InputStream archiveStream, String outputPath) throws IOException; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java new file mode 100644 index 00000000000..ef16c69ea1e --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java @@ -0,0 +1,30 @@ +package com.netgrif.application.engine.workflow.domain; + +import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.util.*; + +public class CaseExportFiles { + + private final Map, Set>>> caseFileMapping = new HashMap<>(); + + public void addFieldFilenames(String caseId, StorageField storageField, Set filenames) { + if (!caseFileMapping.containsKey(caseId)) { + caseFileMapping.put(caseId, new ArrayList<>()); + } + List, Set>> fieldMapping = caseFileMapping.get(caseId); + if (fieldMapping == null) { + fieldMapping = new ArrayList<>(); + } + fieldMapping.add(new ImmutablePair<>(storageField, filenames)); + } + + public Set getCaseIds() { + return caseFileMapping.keySet(); + } + + public List, Set>> getFieldsOfCase(String caseId) { + return caseFileMapping.get(caseId); + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java index abe7e1e0d5f..3c1e4410cf0 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java @@ -1,22 +1,27 @@ package com.netgrif.application.engine.workflow.service; import com.netgrif.application.engine.archive.interfaces.IArchiveService; +import com.netgrif.application.engine.files.IStorageResolverService; +import com.netgrif.application.engine.files.interfaces.IStorageService; +import com.netgrif.application.engine.files.throwable.StorageException; +import com.netgrif.application.engine.petrinet.domain.dataset.*; import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.FileStorageConfiguration; +import com.netgrif.application.engine.workflow.domain.CaseExportFiles; +import com.netgrif.application.engine.workflow.domain.DataField; import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import net.lingala.zip4j.ZipFile; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.DirectoryFileFilter; +import org.apache.commons.io.filefilter.FileFileFilter; import org.springframework.beans.factory.ObjectFactory; import org.springframework.stereotype.Service; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; @Service @@ -27,8 +32,8 @@ public class CaseExportImportService implements ICaseExportImportService { private final ObjectFactory caseImporterObjectFactory; private final ObjectFactory caseExporterObjectFactory; private final IArchiveService archiveService; - private final FileStorageConfiguration fileStorageConfiguration; private final IWorkflowService workflowService; + private final IStorageResolverService storageResolverService; protected CaseImporter getCaseImporter() { return caseImporterObjectFactory.getObject(); @@ -39,45 +44,97 @@ protected CaseExporter getCaseExporter() { } @Override - public void findAndExportCases(Set caseIdsToExport, OutputStream exportFile) { - this.exportCases(caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()), exportFile); + public void findAndExportCases(Set caseIdsToExport, OutputStream exportStream) throws IOException { + this.exportCases(caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()), exportStream); } @Override - public void exportCases(Set casesToExport, OutputStream exportFile) { - this.exportCasesToFile(casesToExport, exportFile); + public void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveStream) throws IOException { + Set casesToExport = caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()); +// todo filename to property? + File exportFile = new File(Files.createTempDirectory("case_export").toFile(), "case_export.xml"); + this.exportCases(casesToExport, new FileOutputStream(exportFile)); + CaseExportFiles caseFiles = this.getFileNamesOfCases(casesToExport); + archiveService.pack(archiveStream, caseFiles, exportFile.getPath()); + FileUtils.deleteDirectory(exportFile.getParentFile()); } @Override - public Set getFilesOfCases(Set casesToExport) { - return Set.of(); + public void exportCases(Set casesToExport, OutputStream exportStream) throws IOException { + this.exportCasesToFile(casesToExport, exportStream); + exportStream.close(); } @Override - public ZipFile zipExportFileAndCaseFiles(OutputStream exportFile, Set caseFiles) { - return null; + public CaseExportFiles getFileNamesOfCases(Set casesToExport) { + CaseExportFiles filesToExport = new CaseExportFiles(); + for (Case exportCase : casesToExport) { + List fields = exportCase.getPetriNet().getDataSet().values().stream() + .filter(field -> field instanceof StorageField) + .map(Field::getStringId).toList(); + fields.forEach(fieldId -> { + DataField dataField = exportCase.getDataField(fieldId); + if (dataField == null || dataField.getValue() == null) { + return; + } + Field field = exportCase.getField(fieldId); + HashSet namesPaths = new HashSet<>(); + if (field instanceof FileListField) { + ((FileListFieldValue) dataField.getValue()).getNamesPaths().forEach(value -> namesPaths.add(value.getName())); + } else { + namesPaths.add(((FileFieldValue) dataField.getValue()).getName()); + } + filesToExport.addFieldFilenames(exportCase.getStringId(), (StorageField) field, namesPaths); + }); + } + return filesToExport; } + @Override + public List importCases(InputStream inputStream) { + return getCaseImporter().importCases(inputStream); + } @Override - public File exportCasesWithFiles(Collection casesToExport, File exportFile) { - try (FileOutputStream fos = new FileOutputStream(exportFile)) { - this.exportCasesToFile(casesToExport, fos); - String[] filePathsToZip = (String[]) casesToExport.stream() - .map(exportCase -> fileStorageConfiguration.getStoragePath() + "/" + exportCase.getStringId()) - .filter(filePath -> Files.exists(Paths.get(filePath))) - .toArray(); -// todo archive name/path - archiveService.pack(fileStorageConfiguration.getStoragePath() + "/exports", filePathsToZip); - } catch (IOException e) { - throw new RuntimeException(e); + public List importCasesWithFiles(InputStream importZipFile) throws IOException { + String directoryPath = archiveService.unpack(importZipFile, Files.createTempDirectory(UUID.randomUUID().toString()).toString()); + importZipFile.close(); + File caseExportXmlFile = FileUtils.getFile(new File(directoryPath.concat(File.separator).concat("case_export.xml"))); + if (!caseExportXmlFile.exists() || caseExportXmlFile.isDirectory()) { +// todo exception handling + throw new RuntimeException("Could not find case export file"); } - return null; + FileInputStream fis = new FileInputStream(caseExportXmlFile); + List importedCases = this.importCases(fis); + fis.close(); + List caseFilesDirectories = Arrays.stream(Objects.requireNonNull(new File(directoryPath).list(DirectoryFileFilter.DIRECTORY))) + .map(caseDirectory -> directoryPath.concat(File.separator).concat(caseDirectory)) + .toList(); + saveFiles(caseFilesDirectories); + FileUtils.deleteDirectory(caseExportXmlFile.getParentFile()); + return importedCases; } - @Override - public List importCases(InputStream inputStream) { - return caseImporterObjectFactory.getObject().importCases(inputStream); + private void saveFiles(List casesDirectories) { + casesDirectories.forEach(caseDirectory -> { + Case importedCase = workflowService.findOne(Paths.get(caseDirectory).getFileName().toString()); + List fieldOfCaseDirectories = Arrays.stream(Objects.requireNonNull(new File(caseDirectory).list(DirectoryFileFilter.DIRECTORY))).toList(); + fieldOfCaseDirectories.forEach(fieldDirectory -> { + String fieldDirectoryPath = caseDirectory.concat(File.separator).concat(fieldDirectory); + StorageField field = (StorageField) importedCase.getField(Paths.get(fieldDirectoryPath).getFileName().toString()); + IStorageService storageService = storageResolverService.resolve(field.getStorageType()); + Arrays.stream(Objects.requireNonNull(new File(fieldDirectoryPath).list(FileFileFilter.FILE))).toList().forEach(fileName -> { + String filePath = fieldDirectoryPath.concat(File.separator).concat(fileName); + String path = storageService.getPath(importedCase.getStringId(), field.getStringId(), fileName); + try (FileInputStream fis = new FileInputStream(filePath)) { + storageService.save(field, path, fis); +// todo error handling + } catch (IOException | StorageException e) { + throw new RuntimeException(e); + } + }); + }); + }); } private void exportCasesToFile(Collection casesToExport, OutputStream exportFile) { diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java index c778f866b9d..ac3bf16d81d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java @@ -40,6 +40,7 @@ public class CaseExporter { private OutputStream outputStream; private com.netgrif.application.engine.workflow.domain.Case caseToExport; private Case xmlCase; + private HashMap translations; // todo custom error handling? public void exportCases(Collection casesToExport, OutputStream outputStream) throws RuntimeException { @@ -48,6 +49,7 @@ public void exportCases(Collection { this.caseToExport = caseToExport; + this.translations = new HashMap<>(); xmlCases.getCase().add(exportCase()); }); @@ -58,14 +60,82 @@ public void exportCases(Collection tasksToExport = caseToExport.getTasks().stream() + .map(taskPair -> taskService.findOne(taskPair.getTask())) + .toList(); + tasksToExport.forEach(taskToExport -> this.xmlCase.getTask().add(exportTask(taskToExport))); + } + + private Task exportTask(com.netgrif.application.engine.workflow.domain.Task taskToExport) { + Task xmlTask = objectFactory.createTask(); + xmlTask.setId(taskToExport.getStringId()); + xmlTask.setTransitionId(taskToExport.getTransitionId()); + xmlTask.setTitle(exportI18NString(taskToExport.getTitle())); + xmlTask.setPriority(exportInteger(taskToExport.getPriority())); + xmlTask.setUserId(taskToExport.getUserId()); + xmlTask.setStartDate(exportLocalDateTime(taskToExport.getStartDate())); + xmlTask.setFinishDate(exportLocalDateTime(taskToExport.getFinishDate())); + xmlTask.setFinishedBy(taskToExport.getFinishedBy()); + xmlTask.setTransactionId(taskToExport.getTransactionId()); + xmlTask.setIcon(taskToExport.getIcon()); + xmlTask.setAssignPolicy(AssignPolicy.fromValue(taskToExport.getAssignPolicy().toString().toLowerCase())); + xmlTask.setDataFocusPolicy(DataFocusPolicy.fromValue(taskToExport.getDataFocusPolicy().toString().toLowerCase())); + xmlTask.setFinishPolicy(FinishPolicy.fromValue(taskToExport.getFinishPolicy().toString().toLowerCase())); + xmlTask.setTags(exportTags(taskToExport.getTags())); + xmlTask.setViewRoles(exportCollectionOfStrings(taskToExport.getViewRoles())); + xmlTask.setViewUserRefs(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setNegativeViewRoles(exportCollectionOfStrings(taskToExport.getNegativeViewRoles())); + xmlTask.setNegativeViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); + xmlTask.setImmediateDataFields(exportCollectionOfStrings(taskToExport.getImmediateDataFields())); + xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); + xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); + xmlTask.setUser(exportPermissions(taskToExport.getUsers())); + xmlTask.setAssignedUserPolicies(exportAssignedUserPolicy(taskToExport.getAssignedUserPolicy())); + xmlTask.setTriggers(exportTriggers(taskToExport.getTriggers())); + return xmlTask; + } + private void exportDataFields() { LinkedHashMap dataSet = this.caseToExport.getDataSet(); if (dataSet == null || dataSet.isEmpty()) { @@ -79,7 +149,7 @@ private DataField exportDataField(String fieldId, com.netgrif.application.engine xmlDataField.setId(fieldId); xmlDataField.setType(DataType.fromValue(caseToExport.getField(fieldId).getType().getName())); xmlDataField.setValues(exportDataFieldValue(dataFieldToExport.getValue(), caseToExport.getField(fieldId).getType())); - if(this.caseToExport.getField(fieldId).getType().equals(FieldType.FILTER)) { + if (this.caseToExport.getField(fieldId).getType().equals(FieldType.FILTER)) { xmlDataField.setFilterMetadata(exportFilterMetadata(dataFieldToExport.getFilterMetadata())); } xmlDataField.setEncryption(dataFieldToExport.getEncryption()); @@ -101,20 +171,20 @@ private FilterMetadata exportFilterMetadata(Map filterMetadata) } FilterMetadata xmlMetadata = objectFactory.createFilterMetadata(); xmlMetadata.setFilterType(FilterType.fromValue(filterMetadata.get("filterType").toString())); - xmlMetadata.setPredicateMetadata(parsePredicateTreeMetadata((List>)filterMetadata.get("predicateTreeMetadata"))); - xmlMetadata.getSearchCategories().getValue().addAll((List)filterMetadata.get("searchCategories")); + xmlMetadata.setPredicateMetadata(parsePredicateTreeMetadata((List>) filterMetadata.get("predicateTreeMetadata"))); + xmlMetadata.getSearchCategories().getValue().addAll((List) filterMetadata.get("searchCategories")); xmlMetadata.setDefaultSearchCategories((Boolean) filterMetadata.get("defaultSearchCategories")); xmlMetadata.setInheritAllowedNets((Boolean) filterMetadata.get("inheritAllowedNets")); return xmlMetadata; } private PredicateTreeMetadata parsePredicateTreeMetadata(List> predicateTreeMetadata) { - if(predicateTreeMetadata == null || predicateTreeMetadata.isEmpty()) { + if (predicateTreeMetadata == null || predicateTreeMetadata.isEmpty()) { return null; } PredicateTreeMetadata xmlPredicateTreeMetadata = objectFactory.createPredicateTreeMetadata(); predicateTreeMetadata.forEach(list -> { - if(list == null || list.isEmpty()) { + if (list == null || list.isEmpty()) { return; } PredicateMetadataArray metadataArray = objectFactory.createPredicateMetadataArray(); @@ -126,7 +196,7 @@ private PredicateTreeMetadata parsePredicateTreeMetadata(List> pred Map retypedData = (Map) data; metadata.setCategory(retypedData.get("category").toString()); metadata.setValues(exportCollectionOfStrings((Collection) retypedData.get("category"))); - metadata.setConfiguration(exportMetadataConfiguration((Map)retypedData.get("configuration"))); + metadata.setConfiguration(exportMetadataConfiguration((Map) retypedData.get("configuration"))); metadataArray.getData().add(metadata); }); xmlPredicateTreeMetadata.getPredicate().add(metadataArray); @@ -140,10 +210,10 @@ private CategoryMetadataConfiguration exportMetadataConfiguration(Map { - ConfigurationValue xmlValue = objectFactory.createConfigurationValue(); - xmlValue.setValue(value); - xmlValue.setId(key); - xmlMetadata.getValue().add(xmlValue); + ConfigurationValue xmlValue = objectFactory.createConfigurationValue(); + xmlValue.setValue(value); + xmlValue.setId(key); + xmlMetadata.getValue().add(xmlValue); }); return xmlMetadata; } @@ -156,9 +226,7 @@ private TaskBehaviors exportTaskBehaviors(Map> taskBe taskBehavior.forEach((taskId, behaviors) -> { Behaviors xmlBehavior = objectFactory.createBehaviors(); xmlBehavior.setTaskId(taskId); - behaviors.forEach(behavior -> { - xmlBehavior.getBehavior().add(Behavior.fromValue(behavior.toString())); - }); + behaviors.forEach(behavior -> xmlBehavior.getBehavior().add(Behavior.fromValue(behavior.toString()))); xmlTaskBehaviors.getTaskBehavior().add(xmlBehavior); }); return xmlTaskBehaviors; @@ -175,10 +243,26 @@ private Options exportOptions(Map options) { xmlOption.setName(option.getKey()); xmlOption.setValue(option.getDefaultValue()); xmlOptions.getOption().add(xmlOption); + exportTranslations(option.getTranslations(), option.getKey()); }); return xmlOptions; } + private void exportTranslations(Map translations, String name) { + translations.forEach((locale, translation) -> { + I18N localeTranslations = this.translations.get(locale); + if (localeTranslations == null) { + localeTranslations = objectFactory.createI18N(); + localeTranslations.setLocale(locale); + this.translations.put(locale, localeTranslations); + } + I18NStringType i18nStringType = objectFactory.createI18NStringType(); + i18nStringType.setValue(translation); + i18nStringType.setName(name); + localeTranslations.getI18NString().add(i18nStringType); + }); + } + private Validations exportValidations(List validations) { if (validations == null || validations.isEmpty()) { return null; @@ -252,14 +336,15 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { switch (type) { case DATE: case DATETIME: - values.getValue().add(exportLocalDateTime(value instanceof LocalDate ? ((LocalDate) value).atTime(LocalTime.NOON) : (LocalDateTime) value)); + LocalDateTime localDateTime = value instanceof Date ? convertDateToLocalDateTime((Date) value) : (value instanceof LocalDate ? ((LocalDate) value).atTime(LocalTime.NOON) : (LocalDateTime) value); + values.getValue().add(exportLocalDateTime(localDateTime)); break; case CASE_REF: case TASK_REF: case MULTICHOICE: case STRING_COLLECTION: case MULTICHOICE_MAP: - ((Collection) value).forEach(it ->values.getValue().add(it.toString())); + ((Collection) value).forEach(it -> values.getValue().add(it.toString())); break; case USER: case USERLIST: @@ -269,9 +354,7 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { } else { userFieldValues = ((UserListFieldValue) value).getUserValues(); } - userFieldValues.forEach(userFieldValue -> { - values.getValue().add(userFieldValue.getId()); - }); + userFieldValues.forEach(userFieldValue -> values.getValue().add(userFieldValue.getId())); break; case FILE: case FILELIST: @@ -281,9 +364,12 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { } else { fileFieldValues = ((FileListFieldValue) value).getNamesPaths(); } - fileFieldValues.forEach(fieldValue -> { - values.getValue().add(fieldValue.getName().concat(":").concat(fieldValue.getPath())); - }); + fileFieldValues.forEach(fieldValue -> values.getValue().add(fieldValue.getName())); + break; + case I18N: + exportI18NString((I18nString) value); + values.getValue().add(value.toString()); + values.setId(((I18nString) value).getKey()); break; default: values.getValue().add(value.toString()); @@ -292,41 +378,10 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { return values; } - private void exportTasks() { - List tasksToExport = caseToExport.getTasks().stream() - .map(taskPair -> taskService.findOne(taskPair.getTask())) - .toList(); - tasksToExport.forEach(taskToExport -> this.xmlCase.getTask().add(exportTask(taskToExport))); - } - - private Task exportTask(com.netgrif.application.engine.workflow.domain.Task taskToExport) { - Task xmlTask = objectFactory.createTask(); - xmlTask.setId(taskToExport.getStringId()); - xmlTask.setTransitionId(taskToExport.getTransitionId()); - xmlTask.setTitle(exportI18NString(taskToExport.getTitle())); - xmlTask.setPriority(exportInteger(taskToExport.getPriority())); - xmlTask.setUserId(taskToExport.getUserId()); - xmlTask.setStartDate(exportLocalDateTime(taskToExport.getStartDate())); - xmlTask.setFinishDate(exportLocalDateTime(taskToExport.getFinishDate())); - xmlTask.setFinishedBy(taskToExport.getFinishedBy()); - xmlTask.setTransactionId(taskToExport.getTransactionId()); - xmlTask.setIcon(taskToExport.getIcon()); - xmlTask.setAssignPolicy(AssignPolicy.fromValue(taskToExport.getAssignPolicy().toString().toLowerCase())); - xmlTask.setDataFocusPolicy(DataFocusPolicy.fromValue(taskToExport.getDataFocusPolicy().toString().toLowerCase())); - xmlTask.setFinishPolicy(FinishPolicy.fromValue(taskToExport.getFinishPolicy().toString().toLowerCase())); - xmlTask.setTags(exportTags(taskToExport.getTags())); - xmlTask.setViewRoles(exportCollectionOfStrings(taskToExport.getViewRoles())); - xmlTask.setViewUserRefs(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); - xmlTask.setViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); - xmlTask.setNegativeViewRoles(exportCollectionOfStrings(taskToExport.getNegativeViewRoles())); - xmlTask.setNegativeViewUsers(exportCollectionOfStrings(taskToExport.getNegativeViewUsers())); - xmlTask.setImmediateDataFields(exportCollectionOfStrings(taskToExport.getImmediateDataFields())); - xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); - xmlTask.setUserRef(exportPermissions(taskToExport.getUserRefs())); - xmlTask.setUser(exportPermissions(taskToExport.getUsers())); - xmlTask.setAssignedUserPolicies(exportAssignedUserPolicy(taskToExport.getAssignedUserPolicy())); - xmlTask.setTriggers(exportTriggers(taskToExport.getTriggers())); - return xmlTask; + private LocalDateTime convertDateToLocalDateTime(Date value) { + return value.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); } private BigInteger exportInteger(Integer priority) { @@ -361,41 +416,6 @@ private AssignedUserPolicies exportAssignedUserPolicy(Map assig return assignedUserPolicies; } - protected void marshallCase(com.netgrif.application.engine.importer.model.Cases caseToExport) throws JAXBException { - JAXBContext jaxbContext = JAXBContext.newInstance(com.netgrif.application.engine.importer.model.Cases.class); - Marshaller marshaller = jaxbContext.createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, schemaLocation); - marshaller.marshal(caseToExport, this.outputStream); - } - - private void exportCaseMetadata(com.netgrif.application.engine.workflow.domain.Case caseToExport) { - this.xmlCase.setId(caseToExport.getStringId()); -// todo should the whole object be exported? is id specific enough? should email be exported instead of id? - this.xmlCase.setAuthor(caseToExport.getAuthor().getId()); - this.xmlCase.setColor(caseToExport.getColor()); - this.xmlCase.setProcessVersion(caseToExport.getPetriNet().getVersion().toString()); - this.xmlCase.setProcessIdentifier(caseToExport.getProcessIdentifier()); - this.xmlCase.setVisualId(caseToExport.getVisualId()); - this.xmlCase.setUriNodeId(caseToExport.getUriNodeId()); - this.xmlCase.setTags(exportTags(caseToExport.getTags())); - this.xmlCase.setTitle(caseToExport.getTitle()); - this.xmlCase.setCreationDate(exportLocalDateTime(caseToExport.getCreationDate())); - this.xmlCase.setLastModified(exportLocalDateTime(caseToExport.getLastModified())); - this.xmlCase.setEnabledRoles(exportCollectionOfStrings(caseToExport.getEnabledRoles())); - this.xmlCase.setViewRoles(exportCollectionOfStrings(caseToExport.getViewRoles())); - this.xmlCase.setViewUserRefs(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - this.xmlCase.setViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - this.xmlCase.setNegativeViewRoles(exportCollectionOfStrings(caseToExport.getNegativeViewRoles())); - this.xmlCase.setNegativeViewUsers(exportCollectionOfStrings(caseToExport.getNegativeViewUsers())); - this.xmlCase.setImmediateDataFields(exportCollectionOfStrings(caseToExport.getImmediateDataFields())); - this.xmlCase.setActivePlaces(exportMapXsdType(caseToExport.getActivePlaces())); - this.xmlCase.setConsumedTokens(exportMapXsdType(caseToExport.getConsumedTokens())); - this.xmlCase.setPermissions(exportPermissions(caseToExport.getPermissions())); - this.xmlCase.setUserRefs(exportPermissions(caseToExport.getUserRefs())); - this.xmlCase.setUsers(exportPermissions(caseToExport.getUsers())); - } - private Tags exportTags(Map tags) { if (tags == null || tags.isEmpty()) { return null; @@ -415,9 +435,10 @@ private I18NStringType exportI18NString(I18nString i18nString) { return null; } I18NStringType i18NStringType = objectFactory.createI18NStringType(); +// todo i18n name is not kept after petri net import, i18n import needs to be refactored as a whole i18NStringType.setName(i18nString.getKey()); i18NStringType.setValue(i18nString.getDefaultValue()); -// todo export translations too probably + exportTranslations(i18nString.getTranslations(), i18nString.getKey()); return i18NStringType; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java index 7330ffbd616..d679f79860a 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java @@ -2,28 +2,27 @@ import com.netgrif.application.engine.auth.domain.IUser; import com.netgrif.application.engine.auth.service.interfaces.IUserService; +import com.netgrif.application.engine.files.StorageResolverService; +import com.netgrif.application.engine.files.interfaces.IStorageService; import com.netgrif.application.engine.importer.model.*; import com.netgrif.application.engine.importer.service.ComponentFactory; -import com.netgrif.application.engine.importer.service.TriggerFactory; +import com.netgrif.application.engine.petrinet.domain.*; import com.netgrif.application.engine.petrinet.domain.Component; -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.domain.dataset.UserFieldValue; -import com.netgrif.application.engine.petrinet.domain.dataset.UserListFieldValue; +import com.netgrif.application.engine.petrinet.domain.Transaction; +import com.netgrif.application.engine.petrinet.domain.Transition; +import com.netgrif.application.engine.petrinet.domain.dataset.*; import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior; import com.netgrif.application.engine.petrinet.domain.dataset.logic.validation.Validation; -import com.netgrif.application.engine.petrinet.domain.policies.AssignPolicy; -import com.netgrif.application.engine.petrinet.domain.policies.DataFocusPolicy; -import com.netgrif.application.engine.petrinet.domain.policies.FinishPolicy; +import com.netgrif.application.engine.petrinet.domain.roles.ProcessRole; import com.netgrif.application.engine.petrinet.domain.version.Version; import com.netgrif.application.engine.petrinet.service.PetriNetService; +import com.netgrif.application.engine.petrinet.service.interfaces.IProcessRoleService; import com.netgrif.application.engine.utils.ImporterUtils; import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.domain.DataField; import com.netgrif.application.engine.workflow.domain.ProcessResourceId; import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.workflow.domain.triggers.TimeTrigger; import com.netgrif.application.engine.workflow.domain.triggers.Trigger; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; @@ -31,7 +30,6 @@ import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; import lombok.extern.slf4j.Slf4j; -import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; @@ -54,13 +52,16 @@ public class CaseImporter { private IUserService userService; @Autowired - protected ComponentFactory componentFactory; + private ComponentFactory componentFactory; @Autowired - protected TriggerFactory triggerFactory; + private ITaskService taskService; @Autowired - protected ITaskService taskService; + private IProcessRoleService processRoleService; + + @Autowired + private StorageResolverService storageResolverService; private Cases xmlCases; private Case importedCase; @@ -78,36 +79,36 @@ public List importCases(InputStream xml) { } for (com.netgrif.application.engine.importer.model.Case xmlCase : xmlCases.getCase()) { importCase(xmlCase); - if (importedCase == null) { + if (this.importedCase == null) { continue; } - importedCases.add(importedCase); + importedCases.add(workflowService.save(this.importedCase)); + this.importedCase = null; } return importedCases; } @Transactional - protected Case importCase(com.netgrif.application.engine.importer.model.Case xmlCase) { + protected void importCase(com.netgrif.application.engine.importer.model.Case xmlCase) { this.xmlCase = xmlCase; Version version = new Version(); PetriNet model = petriNetService.getPetriNet(xmlCase.getProcessIdentifier(), version); if (model == null) { -// todo throw error - log.error("Petri net with identifier [" + xmlCase.getProcessIdentifier() + "] not found, skipping case import"); - return null; +// todo throw error? + log.error("Petri net with identifier [{}] not found, skipping case import", xmlCase.getProcessIdentifier()); + return; } - String importedCaseStringId = xmlCase.getId().split("-")[1].trim(); + ProcessResourceId importedCaseId = new ProcessResourceId(model.getStringId(), xmlCase.getId().split("-")[1].trim()); try { - workflowService.findOne(importedCaseStringId); - this.importedCase = new Case(model, new ProcessResourceId(model.getStringId(), importedCaseStringId.split("-")[1].trim())); - } catch (IllegalArgumentException e) { - log.warn("Case with id [{}] already exists, new id will be generated for imported case", xmlCase.getId()); + workflowService.findOne(importedCaseId.toString()); + log.warn("Case with id [{}] already exists, new id will be generated for imported case", importedCaseId); this.importedCase = new Case(model); + } catch (IllegalArgumentException e) { + this.importedCase = new Case(model, importedCaseId); } importCaseMetadata(); importDataSet(); importTasks(); - return importedCase; } @Transactional @@ -117,51 +118,66 @@ protected void importTasks() { return; } this.xmlCase.getTask().forEach(task -> { - Task importedTask = Task.with() + Transition transition = this.importedCase.getPetriNet().getTransition(task.getTransitionId()); + final Task importedTask = Task.with() + .title(transition.getTitle()) + ._id(new ProcessResourceId(this.importedCase.getPetriNetId(), task.getId().split("-")[1].trim())) .caseId(this.importedCase.getStringId()) - .transitionId(task.getTransitionId()) - .title(parseXmlI18nString(task.getTitle())) - .priority(task.getPriority() != null ? task.getPriority().intValue() : null) + .processId(this.importedCase.getProcessIdentifier()) + .transitionId(transition.getImportId()) + .layout(transition.getLayout()) + .tags(transition.getTags()) .userId(task.getUserId()) .startDate(parseDateTimeFromXml(task.getStartDate())) .finishDate(parseDateTimeFromXml(task.getFinishDate())) .finishedBy(task.getFinishedBy()) - .transactionId(task.getTransactionId()) - .icon(task.getIcon()) - .assignPolicy(AssignPolicy.valueOf(task.getAssignPolicy().value().toUpperCase())) - .dataFocusPolicy(DataFocusPolicy.valueOf(task.getDataFocusPolicy().value().toUpperCase())) - .finishPolicy(FinishPolicy.valueOf(task.getFinishPolicy().value().toUpperCase())) - .tags(task.getTags() != null ? ImporterUtils.buildTagsMap(task.getTags().getTag()) : new HashMap<>()) - .viewRoles(parseStringCollection(task.getViewRoles())) - .viewUserRefs(parseStringCollection(task.getViewUserRefs())) - .viewUsers(parseStringCollection(task.getViewUsers())) - .negativeViewRoles(parseStringCollection(task.getNegativeViewRoles())) - .negativeViewUsers(parseStringCollection(task.getNegativeViewUsers())) - .immediateDataFields(new LinkedHashSet<>(parseStringCollection(task.getImmediateDataFields()))) - .roles(parsePermissionMap(task.getRole())).userRefs(parsePermissionMap(task.getUserRef())) - .users(parsePermissionMap(task.getUser())) - .assignedUserPolicy(task.getAssignedUserPolicies().getAssignedUserPolicy().stream().collect(Collectors.toMap(BooleanMapEntry::getKey, BooleanMapEntry::isValue))) - .triggers(parseXmlTriggers(task.getTriggers())) + .caseColor(this.importedCase.getColor()) + .caseTitle(this.importedCase.getTitle()) + .priority(transition.getPriority()) + .icon(transition.getIcon() == null ? this.importedCase.getIcon() : transition.getIcon()) + .immediateDataFields(new LinkedHashSet<>(transition.getImmediateData())) + .assignPolicy(transition.getAssignPolicy()) + .dataFocusPolicy(transition.getDataFocusPolicy()) + .finishPolicy(transition.getFinishPolicy()) .build(); + transition.getEvents().forEach((type, event) -> importedTask.addEventTitle(type, event.getTitle())); + importedTask.addAssignedUserPolicy(transition.getAssignedUserPolicy()); + for (Trigger trigger : transition.getTriggers()) { + Trigger taskTrigger = trigger.clone(); + importedTask.addTrigger(taskTrigger); + + if (taskTrigger instanceof TimeTrigger timeTrigger) { + taskService.scheduleTaskExecution(importedTask, timeTrigger.getStartDate(), this.importedCase); + } + } + ProcessRole defaultRole = processRoleService.defaultRole(); + ProcessRole anonymousRole = processRoleService.anonymousRole(); + for (Map.Entry> entry : transition.getRoles().entrySet()) { + if (this.importedCase.getEnabledRoles().contains(entry.getKey()) + || defaultRole.getStringId().equals(entry.getKey()) + || anonymousRole.getStringId().equals(entry.getKey())) { + importedTask.addRole(entry.getKey(), entry.getValue()); + } + } + transition.getNegativeViewRoles().forEach(importedTask::addNegativeViewRole); + + for (Map.Entry> entry : transition.getUserRefs().entrySet()) { + importedTask.addUserRef(entry.getKey(), entry.getValue()); + } + importedTask.resolveViewRoles(); + importedTask.resolveViewUserRefs(); + + Transaction transaction = this.importedCase.getPetriNet().getTransactionByTransition(transition); + if (transaction != null) { + importedTask.setTransactionId(transaction.getStringId()); + } + importedTasks.add(importedTask); importedCase.addTask(importedTask); }); taskService.save(importedTasks); } - private List parseXmlTriggers(TaskTrigger triggers) { - List triggerList = new ArrayList<>(); - if (triggers == null) { - return triggerList; - } - triggers.getTrigger().forEach(trigger -> { - Trigger taskTrigger = triggerFactory.buildTrigger(trigger); - taskTrigger.set_id(new ObjectId(trigger.getId())); - triggerList.add(taskTrigger); - }); - return triggerList; - } - @Transactional protected void importDataSet() { xmlCase.getDataField().forEach(field -> { @@ -169,10 +185,10 @@ protected void importDataSet() { dataField.setEncryption(field.getEncryption()); dataField.setLastModified(parseDateTimeFromXml(field.getLastModified())); dataField.setVersion(field.getVersion()); - if(field.getType() == DataType.FILTER) { + if (field.getType() == DataType.FILTER) { dataField.setFilterMetadata(parseFilterMetadata(field.getFilterMetadata())); } - dataField.setValue(parseXmlValue(field.getValues(), field.getType())); + dataField.setValue(parseXmlValue(field)); dataField.setComponent(parseXmlComponent(field.getComponent())); field.getDataRefComponent().stream() .filter(dataRefComponent -> dataRefComponent.getComponent() != null) @@ -198,17 +214,17 @@ private Map parseFilterMetadata(FilterMetadata filterMetadata) { } private Object parsePredicateMetadata(PredicateTreeMetadata predicateMetadata) { - if(predicateMetadata == null || predicateMetadata.getPredicate() == null) { + if (predicateMetadata == null || predicateMetadata.getPredicate() == null) { return null; } List> values = new ArrayList<>(); predicateMetadata.getPredicate().forEach(predicate -> { - if(predicate == null) { + if (predicate == null) { return; } List value = new ArrayList<>(); predicate.getData().forEach(data -> { - if(data == null) { + if (data == null) { return; } Map dataMap = new HashMap<>(); @@ -223,12 +239,12 @@ private Object parsePredicateMetadata(PredicateTreeMetadata predicateMetadata) { } private Object parseConfiguration(CategoryMetadataConfiguration configuration) { - if(configuration == null) { + if (configuration == null) { return null; } Map configurationMap = new HashMap<>(); configuration.getValue().forEach(data -> { - if(data == null) { + if (data == null) { return; } configurationMap.put(data.getId(), data.getValue()); @@ -236,7 +252,9 @@ private Object parseConfiguration(CategoryMetadataConfiguration configuration) { return configurationMap; } - private Object parseXmlValue(StringCollection value, DataType dataType) { + private Object parseXmlValue(com.netgrif.application.engine.importer.model.DataField field) { + StringCollection value = field.getValues(); + DataType dataType = field.getType(); if (value == null || value.getValue() == null || value.getValue().isEmpty()) { return null; } @@ -245,7 +263,7 @@ private Object parseXmlValue(StringCollection value, DataType dataType) { case DATE: case DATE_TIME: parsedValue = parseDateTimeFromXml(value.getValue().getFirst()); - if (dataType == DataType.DATE) { + if (dataType == com.netgrif.application.engine.importer.model.DataType.DATE) { parsedValue = ((LocalDateTime) parsedValue).toLocalDate(); } break; @@ -263,8 +281,12 @@ private Object parseXmlValue(StringCollection value, DataType dataType) { case MULTICHOICE: case MULTICHOICE_MAP: parsedValue = parseStringCollection(value, new LinkedHashSet<>()); - if (dataType == DataType.MULTICHOICE) { - parsedValue = ((Set) parsedValue).stream().map(I18nString::new).collect(Collectors.toCollection(LinkedHashSet::new)); + if (dataType == com.netgrif.application.engine.importer.model.DataType.MULTICHOICE) { + parsedValue = ((Set) parsedValue).stream().map(it -> { + I18nString i18nString = new I18nString(it); + i18nString.setTranslations(parseTranslations(i18nString.getKey())); + return i18nString; + }).collect(Collectors.toCollection(LinkedHashSet::new)); } break; case USER: @@ -274,15 +296,17 @@ private Object parseXmlValue(StringCollection value, DataType dataType) { parsedValue = new UserListFieldValue(value.getValue().stream().map(this::parseUserFieldValue).collect(Collectors.toList())); break; case FILE: -// todo path check/replace if new id for case was generated - parsedValue = FileFieldValue.fromString(value.getValue().getFirst()); + parsedValue = createFileFieldValue(field, value.getValue().getFirst()); break; case FILE_LIST: - parsedValue = FileListFieldValue.fromString(value.getValue().getFirst()); + FileListFieldValue fileListValue = new FileListFieldValue(); + value.getValue().forEach(fileName -> fileListValue.getNamesPaths().add(createFileFieldValue(field, fileName))); + parsedValue = fileListValue; break; case ENUMERATION: case I_18_N: parsedValue = new I18nString(value.getValue().getFirst()); + ((I18nString) parsedValue).setTranslations(parseTranslations(value.getId())); break; case BUTTON: parsedValue = Integer.parseInt(value.getValue().getFirst()); @@ -294,13 +318,27 @@ private Object parseXmlValue(StringCollection value, DataType dataType) { return parsedValue; } + private Map parseTranslations(String name) { + Map translations = new HashMap<>(); + this.xmlCase.getI18N().forEach(i18n -> i18n.getI18NString().stream() + .filter(i18nString -> i18nString.getName().equals(name)) + .forEach(translation -> translations.put(translation.getName(), translation.getName()))); + return translations; + } + + private FileFieldValue createFileFieldValue(com.netgrif.application.engine.importer.model.DataField field, String fileName) { + IStorageService storageService = storageResolverService.resolve(((StorageField) importedCase.getField(field.getId())).getStorageType()); + String path = storageService.getPath(this.importedCase.getStringId(), field.getId(), fileName); + return new FileFieldValue(fileName, path); + } + private UserFieldValue parseUserFieldValue(String xmlValue) { IUser user; try { user = userService.resolveById(xmlValue, true); return new UserFieldValue(user); } catch (IllegalArgumentException e) { - log.error("User with id [" + xmlValue + "] not found, setting empty value"); + log.error("User with id [{}] not found, setting empty value", xmlValue); return new UserFieldValue(); } } @@ -312,9 +350,7 @@ private Map> parseXmlBehaviors(TaskBehaviors behavior } behaviors.getTaskBehavior().forEach(taskBehavior -> { Set behaviorSet = new HashSet<>(); - taskBehavior.getBehavior().forEach(behavior -> { - behaviorSet.add(FieldBehavior.fromString(behavior)); - }); + taskBehavior.getBehavior().forEach(behavior -> behaviorSet.add(FieldBehavior.fromString(behavior))); behaviorMap.put(taskBehavior.getTaskId(), behaviorSet); }); return behaviorMap; @@ -325,9 +361,7 @@ private Map parseXmlOptions(Options options) { if (options == null || options.getOption() == null) { return optionsMap; } - options.getOption().forEach(option -> { - optionsMap.put(option.getKey(), parseXmlI18nString(option)); - }); + options.getOption().forEach(option -> optionsMap.put(option.getKey(), parseXmlI18nString(option))); return optionsMap; } @@ -351,6 +385,7 @@ private I18nString parseXmlI18nString(I18NStringType xmlI18nString) { I18nString parsedI18n = new I18nString(); parsedI18n.setKey(xmlI18nString.getName()); parsedI18n.setDefaultValue(xmlI18nString.getValue()); + parsedI18n.setTranslations(parseTranslations(xmlI18nString.getName())); return parsedI18n; } @@ -376,18 +411,21 @@ protected void importCaseMetadata() { importedCase.setUriNodeId(xmlCase.getUriNodeId()); importedCase.setCreationDate(parseDateTimeFromXml(xmlCase.getCreationDate())); importedCase.setLastModified(parseDateTimeFromXml(xmlCase.getLastModified())); - importedCase.setEnabledRoles(parseStringCollection(xmlCase.getEnabledRoles(), new HashSet<>())); - importedCase.setViewRoles(parseStringCollection(xmlCase.getViewRoles())); importedCase.setViewUserRefs(parseStringCollection(xmlCase.getViewUserRefs())); importedCase.setViewUsers(parseStringCollection(xmlCase.getViewUsers())); - importedCase.setNegativeViewRoles(parseStringCollection(xmlCase.getNegativeViewRoles())); importedCase.setNegativeViewUsers(parseStringCollection(xmlCase.getNegativeViewUsers())); - importedCase.setImmediateDataFields(parseStringCollection(xmlCase.getImmediateDataFields(), new LinkedHashSet<>())); importedCase.setConsumedTokens(parseMapXsdType(xmlCase.getConsumedTokens())); - importedCase.setActivePlaces(parseMapXsdType(xmlCase.getActivePlaces())); - importedCase.setPermissions(parsePermissionMap(xmlCase.getPermissions())); - importedCase.setUserRefs(parsePermissionMap(xmlCase.getUserRefs())); importedCase.setUsers(parsePermissionMap(xmlCase.getUsers())); + importedCase.setTitle(xmlCase.getTitle()); + importedCase.getPetriNet().initializeArcs(importedCase.getDataSet()); + updateCaseState(); + } + + private void updateCaseState() { + Map activePlaces = parseMapXsdType(xmlCase.getActivePlaces()); + this.importedCase.getPetriNet().getPlaces().forEach((placeId, place) -> place.setTokens(activePlaces.getOrDefault(placeId, 0))); + this.importedCase.setActivePlaces(activePlaces); + workflowService.updateMarking(this.importedCase); } private List parseStringCollection(StringCollection xmlCollection) { @@ -419,9 +457,7 @@ private Map parseMapXsdType(MapXsdType consumedTokens) { if (consumedTokens == null) { return map; } - consumedTokens.getEntry().forEach(entry -> { - map.put(entry.getKey(), entry.getValue().intValue()); - }); + consumedTokens.getEntry().forEach(entry -> map.put(entry.getKey(), entry.getValue().intValue())); return map; } @@ -433,12 +469,6 @@ protected void unmarshallXml(InputStream xml) throws JAXBException { xmlCases = (Cases) jaxbUnmarshaller.unmarshal(xml); } - private Version parseProcessVersionFromXml(String xmlVersion) { - String[] versionParts = xmlVersion.split("\\."); -// todo version validation - return new Version(Integer.parseInt(versionParts[0].trim()), Integer.parseInt(versionParts[1].trim()), Integer.parseInt(versionParts[2].trim())); - } - private LocalDateTime parseDateTimeFromXml(String xmlDateTimeString) { return xmlDateTimeString == null ? null : LocalDateTime.parse(xmlDateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java index 8a125be5a37..34e78475e01 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/TaskService.java @@ -612,7 +612,7 @@ void validateData(Transition transition, Case useCase) { } } - protected void scheduleTaskExecution(Task task, LocalDateTime time, Case useCase) { + public void scheduleTaskExecution(Task task, LocalDateTime time, Case useCase) { log.info("[" + useCase.getStringId() + "]: Task " + task.getTitle() + " scheduled to run at " + time.toString()); scheduler.schedule(() -> { try { diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java index 9f75bcef7b3..1535744d637 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java @@ -1,26 +1,25 @@ package com.netgrif.application.engine.workflow.service.interfaces; import com.netgrif.application.engine.workflow.domain.Case; -import net.lingala.zip4j.ZipFile; +import com.netgrif.application.engine.workflow.domain.CaseExportFiles; -import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collection; import java.util.List; import java.util.Set; public interface ICaseExportImportService { - void findAndExportCases(Set caseIdsToExport, OutputStream exportFile); + void findAndExportCases(Set caseIdsToExport, OutputStream exportFile) throws IOException; - void exportCases(Set casesToExport, OutputStream exportFile); + void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveFile) throws IOException; - Set getFilesOfCases(Set casesToExport); + void exportCases(Set casesToExport, OutputStream exportFile) throws IOException; - ZipFile zipExportFileAndCaseFiles(OutputStream exportFile, Set caseFiles); - - File exportCasesWithFiles(Collection casesToExport, File exportFile); + CaseExportFiles getFileNamesOfCases(Set casesToExport); List importCases(InputStream importFile); + + List importCasesWithFiles(InputStream importZipFile) throws IOException; } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java index 84370a74fca..3df1d4435ed 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ITaskService.java @@ -17,6 +17,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.*; public interface ITaskService { @@ -126,4 +127,6 @@ public interface ITaskService { List save(List tasks); SetDataEventOutcome getMainOutcome(Map outcomes, String taskId); + + void scheduleTaskExecution(Task task, LocalDateTime time, Case useCase); } \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java index f144f0eabd2..591911a9d18 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java @@ -1,10 +1,11 @@ package com.netgrif.application.engine.workflow.web; +import com.netgrif.application.engine.workflow.domain.Case; import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -16,36 +17,79 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @Slf4j @RestController() @RequestMapping("/api/case") +@AllArgsConstructor public class CaseImportExportController { - @Autowired - private ICaseExportImportService caseExportImportService; + private final ICaseExportImportService caseExportImportService; - @Operation(summary = "Download xml file containing exported case data", security = {@SecurityRequirement(name = "BasicAuth")}) + @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) @GetMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity exportCase(@RequestParam("caseId") String caseId) { + public ResponseEntity exportCases(@RequestParam("caseIds") Set caseIds) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - caseExportImportService.findAndExportCases(Set.of(caseId), outputStream); +// todo exception handling + try { + caseExportImportService.findAndExportCases(caseIds, outputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"case_export.xml\""); + return ResponseEntity + .ok() + .headers(headers) + .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); + } + + @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) + @GetMapping(value = "/exportWithFiles", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportCasesWithFiles(@RequestParam("caseIds") Set caseIds) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); +// todo exception handling + try { + caseExportImportService.findAndExportCasesWithFiles(caseIds, outputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + caseId + ".xml\""); -//todo delete file after after returning + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"case_export.zip\""); return ResponseEntity .ok() .headers(headers) .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); } - @Operation(summary = "Import case from xml file file", security = {@SecurityRequirement(name = "BasicAuth")}) + @Operation(summary = "Import cases from xml file", security = {@SecurityRequirement(name = "BasicAuth")}) @PostMapping(value = "/import", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity importCase(@RequestPart(value = "file") MultipartFile multipartFile) { + public ResponseEntity importCases(@RequestPart(value = "file") MultipartFile multipartFile) { + List importedCases; + try { + importedCases = caseExportImportService.importCases(multipartFile.getInputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return ResponseEntity + .ok() + .headers(headers) + .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); + } + + @Operation(summary = "Import cases from zip archive", security = {@SecurityRequirement(name = "BasicAuth")}) + @PostMapping(value = "/importWithFiles", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity importCasesWithFiles(@RequestPart(value = "zipFile") MultipartFile multipartZipFile) { + List importedCases; try { - caseExportImportService.importCases(multipartFile.getInputStream()); + importedCases = caseExportImportService.importCasesWithFiles(multipartZipFile.getInputStream()); } catch (IOException e) { throw new RuntimeException(e); } @@ -54,6 +98,6 @@ public ResponseEntity importCase(@RequestPart(value = "file") MultipartF return ResponseEntity .ok() .headers(headers) - .body("Import successful"); + .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); } } diff --git a/src/main/resources/petriNets/petriflow_schema.xsd b/src/main/resources/petriNets/petriflow_schema.xsd index 0e346d4ac9f..716345786ee 100644 --- a/src/main/resources/petriNets/petriflow_schema.xsd +++ b/src/main/resources/petriNets/petriflow_schema.xsd @@ -3,6 +3,7 @@ + @@ -35,6 +36,7 @@ + @@ -44,6 +46,7 @@ + From 62d8c2b9fdadb65c33bf46f522df3a100305511e Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 24 Mar 2025 13:19:20 +0100 Subject: [PATCH 5/9] [NAE-1843] Import/Export services - removal of unused dependency --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index c227ca70b41..5879b7169af 100644 --- a/pom.xml +++ b/pom.xml @@ -568,12 +568,6 @@ minio 8.5.12 - - - net.lingala.zip4j - zip4j - 2.11.5 - From 6bd96633e1fce718ace55b0cb39581cab940d05c Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 31 Mar 2025 08:34:38 +0200 Subject: [PATCH 6/9] [NAE-1843] Import/Export services - CaseImportExportTest tests improved, added test net nae_1843.xml - added nae.case.export.file-name property - improved error handling - multiple bug fixes and small optimizations --- .../engine/archive/ZipService.java | 6 +- .../properties/CaseExportProperties.java | 8 + .../CaseImportExportProperties.java | 15 + .../properties/SchemaProperties.java | 16 + .../workflow/domain/CaseExportFiles.java | 8 +- .../ImportXmlFileMissingException.java | 7 + .../service/CaseExportImportService.java | 148 --- .../engine/workflow/service/CaseExporter.java | 32 +- .../service/CaseImportExportService.java | 164 +++ .../engine/workflow/service/CaseImporter.java | 55 +- .../interfaces/ICaseExportImportService.java | 25 - .../interfaces/ICaseImportExportService.java | 29 + .../web/CaseImportExportController.java | 16 +- src/main/resources/application.properties | 3 + .../engine/workflow/CaseExporterTest.java | 89 -- .../workflow/CaseImportExportTest.groovy | 179 ++++ .../resources/application-test.properties | 5 +- src/test/resources/petriNets/nae_1843.xml | 939 ++++++++++++++++++ 18 files changed, 1447 insertions(+), 297 deletions(-) create mode 100644 src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java create mode 100644 src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java create mode 100644 src/main/java/com/netgrif/application/engine/configuration/properties/SchemaProperties.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/exceptions/ImportXmlFileMissingException.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseImportExportService.java delete mode 100644 src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java create mode 100644 src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy create mode 100644 src/test/resources/petriNets/nae_1843.xml diff --git a/src/main/java/com/netgrif/application/engine/archive/ZipService.java b/src/main/java/com/netgrif/application/engine/archive/ZipService.java index 7fb855507e2..0f979490649 100644 --- a/src/main/java/com/netgrif/application/engine/archive/ZipService.java +++ b/src/main/java/com/netgrif/application/engine/archive/ZipService.java @@ -37,10 +37,10 @@ public void pack(String archivePath, CaseExportFiles caseExportFiles, String... public void pack(OutputStream archiveStream, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException { ZipOutputStream zipStream = new ZipOutputStream(archiveStream); for (String caseId : caseExportFiles.getCaseIds()) { - for (ImmutablePair, Set> fields : caseExportFiles.getFieldsOfCase(caseId)) { - StorageField storageField = fields.left; + for (ImmutablePair, Set> field : caseExportFiles.getFieldsOfCase(caseId)) { + StorageField storageField = field.left; IStorageService storageService = storageResolverService.resolve(storageField.getStorageType()); - for (String fileName : fields.right) { + for (String fileName : field.right) { String filePath = storageService.getPath(caseId, storageField.getStringId(), fileName); InputStream fis = storageService.get(storageField, filePath); String newFileName = caseId.concat(File.separator).concat(storageField.getStringId()).concat(File.separator).concat(fileName); diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java new file mode 100644 index 00000000000..f24e44ffcb4 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java @@ -0,0 +1,8 @@ +package com.netgrif.application.engine.configuration.properties; + +import lombok.Data; + +@Data +public class CaseExportProperties { + private String fileName; +} diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java new file mode 100644 index 00000000000..0ccfed88cb8 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java @@ -0,0 +1,15 @@ +package com.netgrif.application.engine.configuration.properties; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Slf4j +@Data +@Component +@ConfigurationProperties(prefix = "nae.case") +public class CaseImportExportProperties { + + private CaseExportProperties export; +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/SchemaProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/SchemaProperties.java new file mode 100644 index 00000000000..55fa01d4282 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/SchemaProperties.java @@ -0,0 +1,16 @@ +package com.netgrif.application.engine.configuration.properties; + + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Slf4j +@Data +@Component +@ConfigurationProperties(prefix = "nae.schema") +public class SchemaProperties { + + private String location; +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java index ef16c69ea1e..99c348a2278 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java @@ -10,12 +10,10 @@ public class CaseExportFiles { private final Map, Set>>> caseFileMapping = new HashMap<>(); public void addFieldFilenames(String caseId, StorageField storageField, Set filenames) { - if (!caseFileMapping.containsKey(caseId)) { - caseFileMapping.put(caseId, new ArrayList<>()); - } - List, Set>> fieldMapping = caseFileMapping.get(caseId); + List, Set>> emptyFieldMapping = new ArrayList<>(); + List, Set>> fieldMapping = caseFileMapping.putIfAbsent(caseId, emptyFieldMapping); if (fieldMapping == null) { - fieldMapping = new ArrayList<>(); + fieldMapping = emptyFieldMapping; } fieldMapping.add(new ImmutablePair<>(storageField, filenames)); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/exceptions/ImportXmlFileMissingException.java b/src/main/java/com/netgrif/application/engine/workflow/exceptions/ImportXmlFileMissingException.java new file mode 100644 index 00000000000..2d1fbb56c92 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/exceptions/ImportXmlFileMissingException.java @@ -0,0 +1,7 @@ +package com.netgrif.application.engine.workflow.exceptions; + +public class ImportXmlFileMissingException extends Exception { + public ImportXmlFileMissingException(String message) { + super(message); + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java deleted file mode 100644 index 3c1e4410cf0..00000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExportImportService.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.netgrif.application.engine.workflow.service; - -import com.netgrif.application.engine.archive.interfaces.IArchiveService; -import com.netgrif.application.engine.files.IStorageResolverService; -import com.netgrif.application.engine.files.interfaces.IStorageService; -import com.netgrif.application.engine.files.throwable.StorageException; -import com.netgrif.application.engine.petrinet.domain.dataset.*; -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.CaseExportFiles; -import com.netgrif.application.engine.workflow.domain.DataField; -import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; -import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.DirectoryFileFilter; -import org.apache.commons.io.filefilter.FileFileFilter; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.stereotype.Service; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.*; -import java.util.stream.Collectors; - -@Service -@Slf4j -@RequiredArgsConstructor -public class CaseExportImportService implements ICaseExportImportService { - - private final ObjectFactory caseImporterObjectFactory; - private final ObjectFactory caseExporterObjectFactory; - private final IArchiveService archiveService; - private final IWorkflowService workflowService; - private final IStorageResolverService storageResolverService; - - protected CaseImporter getCaseImporter() { - return caseImporterObjectFactory.getObject(); - } - - protected CaseExporter getCaseExporter() { - return caseExporterObjectFactory.getObject(); - } - - @Override - public void findAndExportCases(Set caseIdsToExport, OutputStream exportStream) throws IOException { - this.exportCases(caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()), exportStream); - } - - @Override - public void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveStream) throws IOException { - Set casesToExport = caseIdsToExport.stream().map(workflowService::findOne).collect(Collectors.toSet()); -// todo filename to property? - File exportFile = new File(Files.createTempDirectory("case_export").toFile(), "case_export.xml"); - this.exportCases(casesToExport, new FileOutputStream(exportFile)); - CaseExportFiles caseFiles = this.getFileNamesOfCases(casesToExport); - archiveService.pack(archiveStream, caseFiles, exportFile.getPath()); - FileUtils.deleteDirectory(exportFile.getParentFile()); - } - - @Override - public void exportCases(Set casesToExport, OutputStream exportStream) throws IOException { - this.exportCasesToFile(casesToExport, exportStream); - exportStream.close(); - } - - @Override - public CaseExportFiles getFileNamesOfCases(Set casesToExport) { - CaseExportFiles filesToExport = new CaseExportFiles(); - for (Case exportCase : casesToExport) { - List fields = exportCase.getPetriNet().getDataSet().values().stream() - .filter(field -> field instanceof StorageField) - .map(Field::getStringId).toList(); - fields.forEach(fieldId -> { - DataField dataField = exportCase.getDataField(fieldId); - if (dataField == null || dataField.getValue() == null) { - return; - } - Field field = exportCase.getField(fieldId); - HashSet namesPaths = new HashSet<>(); - if (field instanceof FileListField) { - ((FileListFieldValue) dataField.getValue()).getNamesPaths().forEach(value -> namesPaths.add(value.getName())); - } else { - namesPaths.add(((FileFieldValue) dataField.getValue()).getName()); - } - filesToExport.addFieldFilenames(exportCase.getStringId(), (StorageField) field, namesPaths); - }); - } - return filesToExport; - } - - @Override - public List importCases(InputStream inputStream) { - return getCaseImporter().importCases(inputStream); - } - - @Override - public List importCasesWithFiles(InputStream importZipFile) throws IOException { - String directoryPath = archiveService.unpack(importZipFile, Files.createTempDirectory(UUID.randomUUID().toString()).toString()); - importZipFile.close(); - File caseExportXmlFile = FileUtils.getFile(new File(directoryPath.concat(File.separator).concat("case_export.xml"))); - if (!caseExportXmlFile.exists() || caseExportXmlFile.isDirectory()) { -// todo exception handling - throw new RuntimeException("Could not find case export file"); - } - FileInputStream fis = new FileInputStream(caseExportXmlFile); - List importedCases = this.importCases(fis); - fis.close(); - List caseFilesDirectories = Arrays.stream(Objects.requireNonNull(new File(directoryPath).list(DirectoryFileFilter.DIRECTORY))) - .map(caseDirectory -> directoryPath.concat(File.separator).concat(caseDirectory)) - .toList(); - saveFiles(caseFilesDirectories); - FileUtils.deleteDirectory(caseExportXmlFile.getParentFile()); - return importedCases; - } - - private void saveFiles(List casesDirectories) { - casesDirectories.forEach(caseDirectory -> { - Case importedCase = workflowService.findOne(Paths.get(caseDirectory).getFileName().toString()); - List fieldOfCaseDirectories = Arrays.stream(Objects.requireNonNull(new File(caseDirectory).list(DirectoryFileFilter.DIRECTORY))).toList(); - fieldOfCaseDirectories.forEach(fieldDirectory -> { - String fieldDirectoryPath = caseDirectory.concat(File.separator).concat(fieldDirectory); - StorageField field = (StorageField) importedCase.getField(Paths.get(fieldDirectoryPath).getFileName().toString()); - IStorageService storageService = storageResolverService.resolve(field.getStorageType()); - Arrays.stream(Objects.requireNonNull(new File(fieldDirectoryPath).list(FileFileFilter.FILE))).toList().forEach(fileName -> { - String filePath = fieldDirectoryPath.concat(File.separator).concat(fileName); - String path = storageService.getPath(importedCase.getStringId(), field.getStringId(), fileName); - try (FileInputStream fis = new FileInputStream(filePath)) { - storageService.save(field, path, fis); -// todo error handling - } catch (IOException | StorageException e) { - throw new RuntimeException(e); - } - }); - }); - }); - } - - private void exportCasesToFile(Collection casesToExport, OutputStream exportFile) { - CaseExporter caseExporter = getCaseExporter(); - try { - caseExporter.exportCases(casesToExport, exportFile); - } catch (RuntimeException e) { - log.error("Error exporting cases", e); - } - } -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java index ac3bf16d81d..ead0c83756f 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.workflow.service; +import com.netgrif.application.engine.configuration.properties.SchemaProperties; import com.netgrif.application.engine.importer.model.*; import com.netgrif.application.engine.importer.model.Properties; import com.netgrif.application.engine.petrinet.domain.I18nString; @@ -32,8 +33,8 @@ public class CaseExporter { @Autowired private ITaskService taskService; - @Value("${nae.schema.location}") - private String schemaLocation; + @Autowired + private SchemaProperties properties; private final ObjectFactory objectFactory = new ObjectFactory(); @@ -64,7 +65,7 @@ protected void marshallCase(com.netgrif.application.engine.importer.model.Cases JAXBContext jaxbContext = JAXBContext.newInstance(com.netgrif.application.engine.importer.model.Cases.class); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, schemaLocation); + marshaller.setProperty(Marshaller.JAXB_NO_NAMESPACE_SCHEMA_LOCATION, properties.getLocation()); marshaller.marshal(caseToExport, this.outputStream); } @@ -159,7 +160,12 @@ private DataField exportDataField(String fieldId, com.netgrif.application.engine xmlDataField.setComponent(exportComponent(dataFieldToExport.getComponent())); xmlDataField.getDataRefComponent().addAll(exportDataRefComponents(dataFieldToExport.getDataRefComponents())); xmlDataField.setValidations(exportValidations(dataFieldToExport.getValidations())); - xmlDataField.setOptions(exportOptions(dataFieldToExport.getOptions())); + if(dataFieldToExport.getOptions() != null) { + xmlDataField.setOptions(exportOptions(dataFieldToExport.getOptions())); + } + if(dataFieldToExport.getChoices() != null) { + xmlDataField.setOptions(exportChoices(dataFieldToExport.getChoices())); + } xmlDataField.setBehaviors(exportTaskBehaviors(dataFieldToExport.getBehavior())); return xmlDataField; } @@ -248,6 +254,21 @@ private Options exportOptions(Map options) { return xmlOptions; } + private Options exportChoices(Set options) { + if (options == null || options.isEmpty()) { + return null; + } + Options xmlOptions = objectFactory.createOptions(); + options.forEach( option -> { + Option xmlOption = objectFactory.createOption(); + xmlOption.setName(option.getKey()); + xmlOption.setValue(option.getDefaultValue()); + xmlOptions.getOption().add(xmlOption); + exportTranslations(option.getTranslations(), option.getKey()); + }); + return xmlOptions; + } + private void exportTranslations(Map translations, String name) { translations.forEach((locale, translation) -> { I18N localeTranslations = this.translations.get(locale); @@ -272,6 +293,9 @@ private Validations exportValidations(List caseImporterObjectFactory; + private final ObjectFactory caseExporterObjectFactory; + private final IArchiveService archiveService; + private final IWorkflowService workflowService; + private final IStorageResolverService storageResolverService; + private final CaseImportExportProperties properties; + private final ITaskService taskService; + + protected CaseImporter getCaseImporter() { + return caseImporterObjectFactory.getObject(); + } + + protected CaseExporter getCaseExporter() { + return caseExporterObjectFactory.getObject(); + } + + @Override + public void findAndExportCases(Set caseIdsToExport, OutputStream exportStream) throws IOException { + this.exportCases(workflowService.findAllById(new ArrayList<>(caseIdsToExport)), exportStream); + } + + @Override + public void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveStream) throws IOException { + this.exportCasesWithFiles(workflowService.findAllById(new ArrayList<>(caseIdsToExport)), archiveStream); + } + + @Override + public void exportCasesWithFiles(List casesToExport, OutputStream archiveStream) throws IOException { + File exportFile = new File(Files.createTempDirectory("case_export").toFile(), properties.getExport().getFileName()); + this.exportCases(casesToExport, new FileOutputStream(exportFile)); + CaseExportFiles caseFiles = this.getFileNamesOfCases(casesToExport); + archiveService.pack(archiveStream, caseFiles, exportFile.getPath()); + FileUtils.deleteDirectory(exportFile.getParentFile()); + } + + @Override + public void exportCases(List casesToExport, OutputStream exportStream) throws IOException { + this.exportCasesToFile(casesToExport, exportStream); + exportStream.close(); + } + + @Override + public CaseExportFiles getFileNamesOfCases(List casesToExport) { + CaseExportFiles filesToExport = new CaseExportFiles(); + for (Case exportCase : casesToExport) { + exportCase.getPetriNet().getDataSet().values().stream() + .filter(field -> field instanceof StorageField) + .forEach(field -> { + DataField dataField = exportCase.getDataField(field.getStringId()); + if (dataField == null || dataField.getValue() == null) { + return; + } + HashSet namesPaths = new HashSet<>(); + if (field instanceof FileListField) { + ((FileListFieldValue) dataField.getValue()).getNamesPaths().forEach(value -> namesPaths.add(value.getName())); + } else { + namesPaths.add(((FileFieldValue) dataField.getValue()).getName()); + } + filesToExport.addFieldFilenames(exportCase.getStringId(), (StorageField) field, namesPaths); + }); + } + return filesToExport; + } + + @Override + public List importCases(InputStream inputStream) { + CaseImporter importer = getCaseImporter(); + List importedCases = importer.importCases(inputStream); + if (importedCases.isEmpty()) { + return importedCases; + } + return saveImportedObjects(importedCases,importer.getImportedTasksMap()); + } + + @Override + public List importCasesWithFiles(InputStream importZipStream) throws IOException, StorageException, ImportXmlFileMissingException { + String directoryPath = archiveService.unpack(importZipStream, Files.createTempDirectory(UUID.randomUUID().toString()).toString()); + importZipStream.close(); + File caseExportXmlFile = FileUtils.getFile(new File(directoryPath.concat(File.separator).concat(properties.getExport().getFileName()))); + if (!caseExportXmlFile.exists() || caseExportXmlFile.isDirectory()) { + throw new ImportXmlFileMissingException("Xml import file with name [" + properties.getExport().getFileName() + "] not found in archive"); + } + FileInputStream fis = new FileInputStream(caseExportXmlFile); + CaseImporter importer = getCaseImporter(); + List importedCases = importer.importCases(fis); + fis.close(); + if (importedCases.isEmpty()) { + return importedCases; + } + importedCases = saveImportedObjects(importedCases,importer.getImportedTasksMap()); + List caseFilesDirectories = Arrays.stream(Objects.requireNonNull(new File(directoryPath).list(DirectoryFileFilter.DIRECTORY))) + .map(caseDirectory -> directoryPath.concat(File.separator).concat(caseDirectory)) + .toList(); + saveFiles(caseFilesDirectories, importer.getImportedIdsMapping()); + FileUtils.forceDelete(caseExportXmlFile.getParentFile()); + return importedCases; + } + + private void saveFiles(List casesDirectories, Map importedIdsMapping) throws IOException, StorageException { + for (String caseDirectory : casesDirectories) { + String importedCaseId = importedIdsMapping.get(Paths.get(caseDirectory).getFileName().toString()); + Case importedCase = workflowService.findOne(importedCaseId); + List fieldsOfCaseDirectories = Arrays.stream(Objects.requireNonNull(new File(caseDirectory).list(DirectoryFileFilter.DIRECTORY))).toList(); + for (String fieldDirectory : fieldsOfCaseDirectories) { + String fieldDirectoryPath = caseDirectory.concat(File.separator).concat(fieldDirectory); + StorageField field = (StorageField) importedCase.getField(Paths.get(fieldDirectoryPath).getFileName().toString()); + IStorageService storageService = storageResolverService.resolve(field.getStorageType()); + for (String fileName : Objects.requireNonNull(new File(fieldDirectoryPath).list(FileFileFilter.FILE))) { + String filePath = fieldDirectoryPath.concat(File.separator).concat(fileName); + String path = storageService.getPath(importedCase.getStringId(), field.getStringId(), fileName); + FileInputStream fis = new FileInputStream(filePath); + storageService.save(field, path, fis); + fis.close(); + } + } + } + } + + private List saveImportedObjects(List importedCases, Map> importedTasks) { + return importedCases.stream().map(importedCase -> { + taskService.save(importedTasks.get(importedCase.getStringId())); + return workflowService.save(importedCase); + }).toList(); + } + + private void exportCasesToFile(Collection casesToExport, OutputStream exportFile) { + CaseExporter caseExporter = getCaseExporter(); + caseExporter.exportCases(casesToExport, exportFile); + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java index d679f79860a..d005314fc2e 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImporter.java @@ -29,6 +29,7 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; @@ -66,7 +67,10 @@ public class CaseImporter { private Cases xmlCases; private Case importedCase; private com.netgrif.application.engine.importer.model.Case xmlCase; - + @Getter + private final Map> importedTasksMap = new HashMap<>(); + @Getter + private final Map importedIdsMapping = new HashMap<>(); @Transactional public List importCases(InputStream xml) { @@ -75,22 +79,19 @@ public List importCases(InputStream xml) { unmarshallXml(xml); } catch (JAXBException e) { log.error("Error unmarshalling input xml file: ", e); - return Collections.emptyList(); + return importedCases; } for (com.netgrif.application.engine.importer.model.Case xmlCase : xmlCases.getCase()) { - importCase(xmlCase); - if (this.importedCase == null) { - continue; - } - importedCases.add(workflowService.save(this.importedCase)); + this.xmlCase = xmlCase; this.importedCase = null; + importCase(); + importedCases.add(this.importedCase); } return importedCases; } @Transactional - protected void importCase(com.netgrif.application.engine.importer.model.Case xmlCase) { - this.xmlCase = xmlCase; + protected void importCase() { Version version = new Version(); PetriNet model = petriNetService.getPetriNet(xmlCase.getProcessIdentifier(), version); if (model == null) { @@ -106,9 +107,10 @@ protected void importCase(com.netgrif.application.engine.importer.model.Case xml } catch (IllegalArgumentException e) { this.importedCase = new Case(model, importedCaseId); } + importedIdsMapping.put(xmlCase.getId(), importedCase.get_id().toString()); importCaseMetadata(); - importDataSet(); importTasks(); + importDataSet(); } @Transactional @@ -174,8 +176,9 @@ protected void importTasks() { importedTasks.add(importedTask); importedCase.addTask(importedTask); + importedIdsMapping.put(task.getId(), importedTask.get_id().toString()); }); - taskService.save(importedTasks); + importedTasksMap.put(this.importedCase.getStringId(), importedTasks); } @Transactional @@ -194,8 +197,16 @@ protected void importDataSet() { .filter(dataRefComponent -> dataRefComponent.getComponent() != null) .forEach(dataRefComponent -> dataField.getDataRefComponents().put(dataRefComponent.getTaskId(), parseXmlComponent(dataRefComponent.getComponent()))); dataField.setValidations(parseXmlValidations(field.getValidations())); - dataField.setOptions(parseXmlOptions(field.getOptions())); + if(field.getType() == DataType.ENUMERATION || field.getType() == DataType.MULTICHOICE) { + dataField.setChoices(new HashSet<>(parseXmlOptions(field.getOptions()).values())); + } + if(field.getType() == DataType.ENUMERATION_MAP || field.getType() == DataType.MULTICHOICE_MAP) { + dataField.setOptions(parseXmlOptions(field.getOptions())); + } dataField.setBehavior(parseXmlBehaviors(field.getBehaviors())); + if(field.getType() == DataType.CASE_REF) { + dataField.setAllowedNets(parseStringCollection(field.getAllowedNets())); + } importedCase.getDataSet().put(field.getId(), dataField); }); } @@ -268,9 +279,13 @@ private Object parseXmlValue(com.netgrif.application.engine.importer.model.DataF } break; case STRING_COLLECTION: + parsedValue = parseStringCollection(value); + break; case CASE_REF: case TASK_REF: - parsedValue = parseStringCollection(value); + parsedValue = parseStringCollection(value).stream() + .filter(this.importedIdsMapping::containsKey) + .map(this.importedIdsMapping::get).toList(); break; case NUMBER: parsedValue = Double.parseDouble(value.getValue().getFirst()); @@ -293,7 +308,15 @@ private Object parseXmlValue(com.netgrif.application.engine.importer.model.DataF parsedValue = parseUserFieldValue(value.getValue().getFirst()); break; case USER_LIST: - parsedValue = new UserListFieldValue(value.getValue().stream().map(this::parseUserFieldValue).collect(Collectors.toList())); + List userFieldValues = value.getValue().stream() + .map(this::parseUserFieldValue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (userFieldValues.isEmpty()) { + parsedValue = null; + } else { + parsedValue = new UserListFieldValue(userFieldValues); + } break; case FILE: parsedValue = createFileFieldValue(field, value.getValue().getFirst()); @@ -338,8 +361,8 @@ private UserFieldValue parseUserFieldValue(String xmlValue) { user = userService.resolveById(xmlValue, true); return new UserFieldValue(user); } catch (IllegalArgumentException e) { - log.error("User with id [{}] not found, setting empty value", xmlValue); - return new UserFieldValue(); + log.warn("User with id [{}] not found, setting empty value", xmlValue); + return null; } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java deleted file mode 100644 index 1535744d637..00000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseExportImportService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.netgrif.application.engine.workflow.service.interfaces; - -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.CaseExportFiles; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; -import java.util.Set; - -public interface ICaseExportImportService { - - void findAndExportCases(Set caseIdsToExport, OutputStream exportFile) throws IOException; - - void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveFile) throws IOException; - - void exportCases(Set casesToExport, OutputStream exportFile) throws IOException; - - CaseExportFiles getFileNamesOfCases(Set casesToExport); - - List importCases(InputStream importFile); - - List importCasesWithFiles(InputStream importZipFile) throws IOException; -} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseImportExportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseImportExportService.java new file mode 100644 index 00000000000..fe578b0de97 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/ICaseImportExportService.java @@ -0,0 +1,29 @@ +package com.netgrif.application.engine.workflow.service.interfaces; + +import com.netgrif.application.engine.files.throwable.StorageException; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.domain.CaseExportFiles; +import com.netgrif.application.engine.workflow.exceptions.ImportXmlFileMissingException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Set; + +public interface ICaseImportExportService { + + void findAndExportCases(Set caseIdsToExport, OutputStream exportStream) throws IOException; + + void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStream archiveStream) throws IOException; + + void exportCasesWithFiles(List caseIdsToExport, OutputStream archiveStream) throws IOException; + + void exportCases(List casesToExport, OutputStream exportStream) throws IOException; + + CaseExportFiles getFileNamesOfCases(List casesToExport); + + List importCases(InputStream importStream); + + List importCasesWithFiles(InputStream importZipStream) throws IOException, StorageException, ImportXmlFileMissingException; +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java index 591911a9d18..6b8ba093712 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java +++ b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java @@ -1,7 +1,10 @@ package com.netgrif.application.engine.workflow.web; +import com.netgrif.application.engine.configuration.properties.CaseImportExportProperties; +import com.netgrif.application.engine.files.throwable.StorageException; import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.service.interfaces.ICaseExportImportService; +import com.netgrif.application.engine.workflow.exceptions.ImportXmlFileMissingException; +import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.AllArgsConstructor; @@ -27,13 +30,13 @@ @AllArgsConstructor public class CaseImportExportController { - private final ICaseExportImportService caseExportImportService; + private final ICaseImportExportService caseExportImportService; + private final CaseImportExportProperties properties; @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) @GetMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity exportCases(@RequestParam("caseIds") Set caseIds) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); -// todo exception handling try { caseExportImportService.findAndExportCases(caseIds, outputStream); } catch (IOException e) { @@ -41,7 +44,7 @@ public ResponseEntity exportCases(@RequestParam("caseIds") Set } HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"case_export.xml\""); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + properties.getExport().getFileName() + "\""); return ResponseEntity .ok() .headers(headers) @@ -52,7 +55,6 @@ public ResponseEntity exportCases(@RequestParam("caseIds") Set @GetMapping(value = "/exportWithFiles", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity exportCasesWithFiles(@RequestParam("caseIds") Set caseIds) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); -// todo exception handling try { caseExportImportService.findAndExportCasesWithFiles(caseIds, outputStream); } catch (IOException e) { @@ -90,8 +92,10 @@ public ResponseEntity importCasesWithFiles(@RequestPart(value = "zipFile List importedCases; try { importedCases = caseExportImportService.importCasesWithFiles(multipartZipFile.getInputStream()); - } catch (IOException e) { + } catch (IOException | StorageException e) { throw new RuntimeException(e); + } catch (ImportXmlFileMissingException e) { + return ResponseEntity.badRequest().body(e.getMessage()); } HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 20b5444e86f..9345721bb6e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -200,3 +200,6 @@ management.info.env.enabled=true #Schema nae.schema.location=https://petriflow.com/petriflow.schema.xsd + +#Case +nae.case.export.file-name=case_export.xml \ No newline at end of file diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java b/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java deleted file mode 100644 index 53ec066cc3a..00000000000 --- a/src/test/groovy/com/netgrif/application/engine/workflow/CaseExporterTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.netgrif.application.engine.workflow; - -import com.netgrif.application.engine.TestHelper; -import com.netgrif.application.engine.petrinet.domain.PetriNet; -import com.netgrif.application.engine.petrinet.domain.VersionType; -import com.netgrif.application.engine.petrinet.domain.throwable.MissingPetriNetMetaDataException; -import com.netgrif.application.engine.petrinet.service.PetriNetService; -import com.netgrif.application.engine.startup.runner.SuperCreatorRunner; -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.service.CaseExporter; -import com.netgrif.application.engine.workflow.service.CaseImporter; -import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; -import lombok.extern.slf4j.Slf4j; -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.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.List; - -@Slf4j -@SpringBootTest -@ActiveProfiles({"test"}) -@ExtendWith(SpringExtension.class) -public class CaseExporterTest { - - private final String testNetFileName = "all_data.xml"; - private final String testNetIdentifier = "all_data"; - private final String outputFileLocation = "src/test/resources/"; - private final String outputFileName = "case_export_test.xml"; - - @Autowired - private SuperCreatorRunner superCreator; - - @Autowired - private CaseExporter caseExporter; - - @Autowired - private PetriNetService petriNetService; - - @Autowired - private IWorkflowService workflowService; - - @Autowired - private TestHelper testHelper; - - @Autowired - private CaseImporter caseImporter; - - private PetriNet petriNet; - - @BeforeEach - public void before() { - testHelper.truncateDbs(); - try (FileInputStream fis = new FileInputStream("src/test/resources/" + testNetFileName)) { - petriNet = petriNetService.importPetriNet(fis, VersionType.MAJOR, superCreator.getLoggedSuper()).getNet(); - } catch (MissingPetriNetMetaDataException | IOException e) { - throw new RuntimeException(e); - } - } - - @Test - public void exportCase() { - Case toExport = workflowService.createCaseByIdentifier(testNetIdentifier, "export case", "", superCreator.getLoggedSuper()).getCase(); - try (FileOutputStream fos = new FileOutputStream(outputFileLocation + outputFileName)) { - caseExporter.exportCases(List.of(toExport), fos); - } catch (IOException e) { - log.error("IO exception occured", e); - } - workflowService.deleteCase(toExport); - } - - @Test - public void importCase() { - try (FileInputStream fis = new FileInputStream(outputFileLocation + outputFileName)) { - List importedCases = caseImporter.importCases(fis); - assert importedCases != null && !importedCases.isEmpty(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} \ No newline at end of file diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy new file mode 100644 index 00000000000..9ff1d445aa5 --- /dev/null +++ b/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy @@ -0,0 +1,179 @@ +package com.netgrif.application.engine.workflow + +import com.netgrif.application.engine.TestHelper +import com.netgrif.application.engine.auth.service.interfaces.IUserService +import com.netgrif.application.engine.files.StorageResolverService +import com.netgrif.application.engine.files.interfaces.IStorageService +import com.netgrif.application.engine.petrinet.domain.I18nString +import com.netgrif.application.engine.petrinet.domain.dataset.StorageField +import com.netgrif.application.engine.petrinet.domain.dataset.logic.FieldBehavior +import com.netgrif.application.engine.petrinet.service.PetriNetService +import com.netgrif.application.engine.startup.ImportHelper +import com.netgrif.application.engine.startup.runner.SuperCreatorRunner +import com.netgrif.application.engine.workflow.domain.Case +import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService +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 +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.LocalDate +import java.time.LocalDateTime +import java.time.Month +import java.time.format.DateTimeFormatter + +@Slf4j +@SpringBootTest +@ActiveProfiles(["test"]) +@ExtendWith(SpringExtension.class) +class CaseImportExportTest { + + private final String testNetFileName = "nae_1843.xml" + private final String testNetIdentifier = "data/export_data" + private final String outputFileLocation = "src" + File.separator + "test" + File.separator + "resources" + File.separator + private final String outputFileName = "case_export_test.zip" + private final String outputPath = outputFileLocation + outputFileName + + @Autowired + private SuperCreatorRunner superCreator + + @Autowired + private PetriNetService petriNetService + + @Autowired + private IWorkflowService workflowService + + @Autowired + private TestHelper testHelper + + @Autowired + private ImportHelper importHelper + + @Autowired + private ICaseImportExportService caseImportExportService + + @Autowired + private StorageResolverService resolverService + + @Autowired + private IUserService userService + + @Autowired + private ITaskService taskService + + @BeforeEach + void before() { + testHelper.truncateDbs() + importHelper.createNet(testNetFileName).get() + } + + @Test + void exportCase() { + Case toExport = createCaseAndSetData() + importHelper.assignTaskToSuper("Start task", toExport.stringId) + toExport = importHelper.finishTaskAsSuper("Start task", toExport.stringId).getCase() + File outputFile = new File(outputPath) + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + caseImportExportService.exportCasesWithFiles(List.of(toExport), fos) + assert outputFile.exists() && !outputFile.isDirectory() + assert outputFile.length() > 0 + } catch (IOException e) { + log.error("IO exception occured", e) + } + workflowService.deleteCase(toExport) + } + + @Test + void importCase() { + try (FileInputStream fis = new FileInputStream(outputPath)) { + List importedCases = caseImportExportService.importCasesWithFiles(fis) + assert importedCases != null && !importedCases.isEmpty() + Case importedCase = importedCases[0] + assertDataImport(importedCase) + assertDataBehavior(importedCase) + assert importedCase.tasks.size() == 2 + assert importedCase.tasks.collect { it.transition }.containsAll(["t1", "t2"]) + } catch (IOException e) { + throw new RuntimeException(e) + } + } + + Case createCaseAndSetData() { + Case testCase = workflowService.createCaseByIdentifier(testNetIdentifier, "export case", "", superCreator.getLoggedSuper()).getCase() + Map dataSet = [ + "number" : ["type": "number", "value": 24.3], + "number_currency" : ["type": "number", "value": 223], + "text" : ["type": "text", "value": "test text"], + "password_data" : ["type": "text", "value": "password"], + "text_area" : ["type": "text", "value": "text area"], + "enumeration_autocomplete": ["type": "enumeration", "value": "Alice"], + "enumeration_list" : ["type": "enumeration", "value": "Alice"], + "enumeration_map" : ["type": "enumeration_map", "value": "al"], + "multichoice" : ["type": "multichoice", "value": ["Alice", "Carol"]], + "multichoice_list" : ["type": "multichoice", "value": ["Alice", "Carol"]], + "multichoice_map" : ["type": "multichoice_map", "value": ["al", "ca"]], + "boolean" : ["type": "boolean", "value": "true"], + "date" : ["type": "date", "value": LocalDate.of(2025, Month.APRIL, 1).format(DateTimeFormatter.ISO_DATE)], + "datetime" : ["type": "dateTime", "value": LocalDateTime.of(2025, Month.APRIL, 1, 17, 23).format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))], + "taskRef" : ["type": "taskRef", "value": [testCase.tasks.find { it.transition == "t2" }.task]], + "test_i18n" : ["type": "i18n", "value": new I18nString("test i18n")], + "user" : ["type": "user", "value": userService.findByEmail("super@netgrif.com", true).stringId], + "userList1" : ["type": "userList", "value": [userService.findByEmail("super@netgrif.com", true).stringId]], + "button" : ["type": "button", "value": 45], + "caseRef_field" : ["type": "caseRef", "value": [testCase.stringId]], + "stringCollection_field" : ["type": "stringCollection", "value": ["test_value_1", "test_value_2"]], + ] + setFiles(testCase, dataSet) + return importHelper.setTaskData(testCase.tasks.find { it.transition == "1" }.task, dataSet).getCase() + } + + void setFiles(Case testCase, Map dataSet) { + StorageField fileField = testCase.getField("file") as StorageField + IStorageService fileStorageService = resolverService.resolve(fileField.getStorageType()) + String fileFieldpath = fileStorageService.getPath(testCase.stringId, fileField.stringId, "arc_order_test.xml") + fileStorageService.save(fileField, fileFieldpath, new FileInputStream(new File(outputFileLocation.concat("arc_order_test.xml")))) + dataSet.put("file", ["type": "file", "value": "arc_order_test.xml:".concat(fileFieldpath)]) + } + + static void assertDataImport(Case importedCase) { + assert importedCase.dataSet["number"].value == 24.3 + assert importedCase.dataSet["number_currency"].value == 223 + assert importedCase.dataSet["text"].value == "test text" + assert importedCase.dataSet["password_data"].value == "password" + assert importedCase.dataSet["text_area"].value == "text area" + assert importedCase.dataSet["enumeration_autocomplete"].value.defaultValue == "Alice" + assert importedCase.dataSet["enumeration"].value.defaultValue == "changed_option" + assert importedCase.dataSet["enumeration"].choices[0].defaultValue == "changed_option" + assert importedCase.dataSet["enumeration_list"].value.defaultValue == "Alice" + assert importedCase.dataSet["enumeration_map"].value == "al" + assert (importedCase.dataSet["multichoice"].value as Set).size() == 2 && (importedCase.dataSet["multichoice"].value as Set).stream().filter { ["Alice", "Carol"].contains(it.defaultValue) }.toList().size() == 2 + assert (importedCase.dataSet["multichoice_list"].value as Set).size() == 2 && (importedCase.dataSet["multichoice_list"].value as Set).stream().filter { ["Alice", "Carol"].contains(it.defaultValue) }.toList().size() == 2 + assert (importedCase.dataSet["multichoice_map"].value as Set).containsAll(["al", "ca"]) + assert importedCase.dataSet["boolean"].value == true + assert importedCase.dataSet["date"].value.equals(LocalDate.of(2025, Month.APRIL, 1)) + assert importedCase.dataSet["datetime"].value.equals(LocalDateTime.of(2025, Month.APRIL, 1, 17, 23)) + assert importedCase.dataSet["taskRef"].value.contains(importedCase.tasks.find { it.transition == "t2" }.task) + assert importedCase.dataSet["test_i18n"].value.defaultValue == "test i18n" + assert importedCase.dataSet["user"].value == null + assert importedCase.dataSet["userList1"].value == null + assert importedCase.dataSet["button"].value == 45 + assert importedCase.dataSet["caseRef_field"].value.contains(importedCase.stringId) + assert importedCase.dataSet["stringCollection_field"].value.containsAll(["test_value_1", "test_value_2"]) + assert importedCase.dataSet["caseRef_change_allowed_nets"].allowedNets.containsAll(["changed_allowed_nets"]) + assert importedCase.dataSet["text"].validations.find { validation -> validation.validationRule == "email" } != null + } + + static void assertDataBehavior(Case importedCase) { + assert importedCase.dataSet["number"].behavior.get("t1").size() == 1 && importedCase.dataSet["number"].behavior.get("t1")[0].name() == FieldBehavior.VISIBLE.name() + assert importedCase.dataSet["text"].behavior.get("t1").size() == 2 && importedCase.dataSet["text"].behavior.get("t1").collect { it.name() }.containsAll([FieldBehavior.EDITABLE.name(), FieldBehavior.REQUIRED.name()]) + assert importedCase.dataSet["enumeration"].behavior.get("t1").size() == 1 && importedCase.dataSet["enumeration"].behavior.get("t1")[0].name() == FieldBehavior.HIDDEN.name() + assert importedCase.dataSet["multichoice"].behavior.get("t1").size() == 1 && importedCase.dataSet["multichoice"].behavior.get("t1")[0].name() == FieldBehavior.FORBIDDEN.name() + assert importedCase.dataSet["date"].behavior.get("t1").size() == 2 && importedCase.dataSet["date"].behavior.get("t1").collect { it.name() }.containsAll([FieldBehavior.EDITABLE.name(), FieldBehavior.OPTIONAL.name()]) + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 23248333d50..8d3be165701 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -73,4 +73,7 @@ nae.public.url=test.public.url nae.storage.minio.enabled=true nae.storage.minio.hosts.host_1.host=http://127.0.0.1:9000 nae.storage.minio.hosts.host_1.user=root -nae.storage.minio.hosts.host_1.password=password \ No newline at end of file +nae.storage.minio.hosts.host_1.password=password + +#Schema +nae.schema.location=${SCHEMA_LOCATION:https://petriflow.com/petriflow.schema.xsd} \ No newline at end of file diff --git a/src/test/resources/petriNets/nae_1843.xml b/src/test/resources/petriNets/nae_1843.xml new file mode 100644 index 00000000000..6c4bcc87358 --- /dev/null +++ b/src/test/resources/petriNets/nae_1843.xml @@ -0,0 +1,939 @@ + + data/export_data + EXP + Export data + true + false + false + Export data test default case name + + process_role + Process role + + + boolean + Boolean + True + + + button + Button + Push + + fab + + + + caseRef_field + CaseRef field + + data/export_data + + + + caseRef_change_allowed_nets + CaseRef field + + data/export_data + + + + date + Date + 01.01.2019 + + + datetime + Datetime + 01.01.2019 20:00 + + + enumeration + Enumeration + + + + + + Bob + + + enumeration_autocomplete + Enumeration autocomplete + + + + + + Bob + + autocomplete + + + + enumeration_list + Enumeration list + + + + + + Bob + + list + + + + enumeration_map + Enumeration Map + + + + + + bo + + + file + File + + + fileList + File List + + + multichoice + Multichoice + + + + + + + Alice + Bob + + + + multichoice_list + Multichoice list + + + + + + Alice,Bob + + list + + + + multichoice_map + Multichoice Map + + + + + + + al + ca + + + + number + Number + 10000 + + + number_currency + Number currency + 10000 + + currency + sk_SK + EUR + 2 + + + + password_data + Password from data + + password + + + + password_dataref + Password from dataRef + + + stringCollection_field + StringCollection field + + + taskRef + Task Ref + + 4 + + + + taskref_test_field + Text datafield z iného transitionu + Field načítaný pomocou taskrefu + + + test_i18n + test + test_default + + + text + Text + Lorem ipsum + + + text_area + Text area + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque vitae magna in libero semper + vulputate ut eu sapien. Phasellus vel. + + textarea + red + 12 + + + + user + User + + + userList1 + UserList + + + userList2 + UserList + + + test_en + + + test_de + + + 1 + 336 + 272 + + auto + + number + 4 + grid + Number fields + + number + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + text + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + enumeration + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + multichoice + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + boolean + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + date + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + file + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + user + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + button + + editable + + + 0 + 0 + 1 + 2 + + outline + + + 1 + + + + + + + + + taskRef + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + number_currency + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + text_area + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + enumeration_autocomplete + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + multichoice_list + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + datetime + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + fileList + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + password_data + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + enumeration_list + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + multichoice_map + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + password_dataref + + editable + + + 0 + 3 + 1 + 2 + + outline + + + password + + + + enumeration_map + + editable + + + 0 + 3 + 1 + 2 + + outline + + + + + 1 + + + trans: t.t1, + number: f.number, + text: f.text, + enumeration: f.enumeration, + multichoice: f.multichoice, + caseRef_change_allowed_nets: f.caseRef_change_allowed_nets, + date: f.date; + + make number,visible on trans when { true } + make text,required on trans when { true } + make enumeration,hidden on trans when { true } + make multichoice,forbidden on trans when { true } + make date,optional on trans when { true } + + change enumeration options { + [ "changed_option": new com.netgrif.application.engine.petrinet.domain.I18nString("changed_option")] + } + + change enumeration value { new com.netgrif.application.engine.petrinet.domain.I18nString("changed_option") } + + change caseRef_change_allowed_nets allowedNets { + ["changed_allowed_nets"] + } + + change text validations { "email" } + + + + + + t1 + 528 + 272 + + + number + 4 + grid + Number fields + + number + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + text + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + enumeration + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + multichoice + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + boolean + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + date + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + file + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + user + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + button + + editable + + + 0 + 0 + 1 + 2 + + outline + + + 2 + + + + + + + + + taskRef + + editable + + + 0 + 0 + 1 + 2 + + outline + + + + number_currency + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + text_area + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + enumeration_autocomplete + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + multichoice_list + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + datetime + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + fileList + + editable + + + 0 + 1 + 1 + 2 + + outline + + + + password_data + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + enumeration_list + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + multichoice_map + + editable + + + 0 + 2 + 1 + 2 + + outline + + + + password_dataref + + editable + + + 0 + 3 + 1 + 2 + + outline + + + password + + + + enumeration_map + + editable + + + 0 + 3 + 1 + 2 + + outline + + + + + + t2 + 336 + 112 + + + + p1 + 240 + 272 + + 1 + false + + + p2 + 432 + 272 + + 0 + false + + + a1 + regular + p1 + 1 + 1 + + + a2 + regular + 1 + p2 + 1 + + + a3 + regular + p2 + t1 + 1 + + \ No newline at end of file From 4e9e16fddd50a09ea6a24fd1d3221c40eb871b44 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 31 Mar 2025 09:15:32 +0200 Subject: [PATCH 7/9] [NAE-1843] Import/Export services - PR comment changes --- .../workflow/CaseImportExportTest.groovy | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy b/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy index 9ff1d445aa5..97a1fceae06 100644 --- a/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy +++ b/src/test/groovy/com/netgrif/application/engine/workflow/CaseImportExportTest.groovy @@ -14,6 +14,7 @@ import com.netgrif.application.engine.workflow.domain.Case import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService import com.netgrif.application.engine.workflow.service.interfaces.ITaskService import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService +import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -30,6 +31,7 @@ import java.time.format.DateTimeFormatter @Slf4j @SpringBootTest +@CompileStatic @ActiveProfiles(["test"]) @ExtendWith(SpringExtension.class) class CaseImportExportTest { @@ -84,7 +86,7 @@ class CaseImportExportTest { assert outputFile.exists() && !outputFile.isDirectory() assert outputFile.length() > 0 } catch (IOException e) { - log.error("IO exception occured", e) + throw new RuntimeException(e) } workflowService.deleteCase(toExport) } @@ -130,7 +132,7 @@ class CaseImportExportTest { "stringCollection_field" : ["type": "stringCollection", "value": ["test_value_1", "test_value_2"]], ] setFiles(testCase, dataSet) - return importHelper.setTaskData(testCase.tasks.find { it.transition == "1" }.task, dataSet).getCase() + return importHelper.setTaskData(testCase.tasks.find { it.transition == "1" }.task, dataSet as Map>).getCase() } void setFiles(Case testCase, Map dataSet) { @@ -147,10 +149,10 @@ class CaseImportExportTest { assert importedCase.dataSet["text"].value == "test text" assert importedCase.dataSet["password_data"].value == "password" assert importedCase.dataSet["text_area"].value == "text area" - assert importedCase.dataSet["enumeration_autocomplete"].value.defaultValue == "Alice" - assert importedCase.dataSet["enumeration"].value.defaultValue == "changed_option" + assert (importedCase.dataSet["enumeration_autocomplete"].value as I18nString).defaultValue == "Alice" + assert (importedCase.dataSet["enumeration"].value as I18nString).defaultValue == "changed_option" assert importedCase.dataSet["enumeration"].choices[0].defaultValue == "changed_option" - assert importedCase.dataSet["enumeration_list"].value.defaultValue == "Alice" + assert (importedCase.dataSet["enumeration_list"].value as I18nString).defaultValue == "Alice" assert importedCase.dataSet["enumeration_map"].value == "al" assert (importedCase.dataSet["multichoice"].value as Set).size() == 2 && (importedCase.dataSet["multichoice"].value as Set).stream().filter { ["Alice", "Carol"].contains(it.defaultValue) }.toList().size() == 2 assert (importedCase.dataSet["multichoice_list"].value as Set).size() == 2 && (importedCase.dataSet["multichoice_list"].value as Set).stream().filter { ["Alice", "Carol"].contains(it.defaultValue) }.toList().size() == 2 @@ -158,13 +160,13 @@ class CaseImportExportTest { assert importedCase.dataSet["boolean"].value == true assert importedCase.dataSet["date"].value.equals(LocalDate.of(2025, Month.APRIL, 1)) assert importedCase.dataSet["datetime"].value.equals(LocalDateTime.of(2025, Month.APRIL, 1, 17, 23)) - assert importedCase.dataSet["taskRef"].value.contains(importedCase.tasks.find { it.transition == "t2" }.task) - assert importedCase.dataSet["test_i18n"].value.defaultValue == "test i18n" + assert (importedCase.dataSet["taskRef"].value as List).contains(importedCase.tasks.find { it.transition == "t2" }.task) + assert (importedCase.dataSet["test_i18n"].value as I18nString).defaultValue == "test i18n" assert importedCase.dataSet["user"].value == null assert importedCase.dataSet["userList1"].value == null assert importedCase.dataSet["button"].value == 45 - assert importedCase.dataSet["caseRef_field"].value.contains(importedCase.stringId) - assert importedCase.dataSet["stringCollection_field"].value.containsAll(["test_value_1", "test_value_2"]) + assert (importedCase.dataSet["caseRef_field"].value as List).contains(importedCase.stringId) + assert (importedCase.dataSet["stringCollection_field"].value as List).containsAll(["test_value_1", "test_value_2"]) assert importedCase.dataSet["caseRef_change_allowed_nets"].allowedNets.containsAll(["changed_allowed_nets"]) assert importedCase.dataSet["text"].validations.find { validation -> validation.validationRule == "email" } != null } From c1c0515283e2e7c6af8c643247b083be450bd85e Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Mon, 28 Apr 2025 16:33:50 +0200 Subject: [PATCH 8/9] [NAE-1843] Import/Export services - implemented changes requested by PR reviewers - CaseImportExportController split into 2 separate controller CaseExportController and CaseImportController - CaseExportFiles refactored to be more readable, added StorageFieldWithFileNames wrapper class - CaseImportExportProperties renamed to CaseImportProperties and removed redundant nested object --- .../engine/archive/ZipService.java | 17 ++- .../properties/CaseExportProperties.java | 9 +- .../CaseImportExportProperties.java | 15 --- .../workflow/domain/CaseExportFiles.java | 15 +-- .../domain/StorageFieldWithFileNames.java | 18 +++ .../engine/workflow/service/CaseExporter.java | 26 ++--- .../service/CaseImportExportService.java | 26 ++--- .../workflow/web/CaseExportController.java | 57 ++++++++++ .../workflow/web/CaseImportController.java | 70 ++++++++++++ .../web/CaseImportExportController.java | 107 ------------------ 10 files changed, 193 insertions(+), 167 deletions(-) delete mode 100644 src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/domain/StorageFieldWithFileNames.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/web/CaseExportController.java create mode 100644 src/main/java/com/netgrif/application/engine/workflow/web/CaseImportController.java delete mode 100644 src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java diff --git a/src/main/java/com/netgrif/application/engine/archive/ZipService.java b/src/main/java/com/netgrif/application/engine/archive/ZipService.java index 0f979490649..1254ecd5909 100644 --- a/src/main/java/com/netgrif/application/engine/archive/ZipService.java +++ b/src/main/java/com/netgrif/application/engine/archive/ZipService.java @@ -5,15 +5,14 @@ import com.netgrif.application.engine.files.interfaces.IStorageService; import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; import com.netgrif.application.engine.workflow.domain.CaseExportFiles; +import com.netgrif.application.engine.workflow.domain.StorageFieldWithFileNames; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.springframework.stereotype.Service; import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Set; import java.util.UUID; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -37,13 +36,13 @@ public void pack(String archivePath, CaseExportFiles caseExportFiles, String... public void pack(OutputStream archiveStream, CaseExportFiles caseExportFiles, String... additionalFiles) throws IOException { ZipOutputStream zipStream = new ZipOutputStream(archiveStream); for (String caseId : caseExportFiles.getCaseIds()) { - for (ImmutablePair, Set> field : caseExportFiles.getFieldsOfCase(caseId)) { - StorageField storageField = field.left; - IStorageService storageService = storageResolverService.resolve(storageField.getStorageType()); - for (String fileName : field.right) { - String filePath = storageService.getPath(caseId, storageField.getStringId(), fileName); - InputStream fis = storageService.get(storageField, filePath); - String newFileName = caseId.concat(File.separator).concat(storageField.getStringId()).concat(File.separator).concat(fileName); + for (StorageFieldWithFileNames fieldWithFileNames : caseExportFiles.getFieldsOfCase(caseId)) { + StorageField field = fieldWithFileNames.getField(); + IStorageService storageService = storageResolverService.resolve(field.getStorageType()); + for (String fileName : fieldWithFileNames.getFileNames()) { + String filePath = storageService.getPath(caseId, field.getStringId(), fileName); + InputStream fis = storageService.get(field, filePath); + String newFileName = Paths.get(caseId, field.getStringId(), fileName).getFileName().toString(); createAndWriteZipEntry(newFileName, zipStream, fis); fis.close(); } diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java index f24e44ffcb4..86670f623fc 100644 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java +++ b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseExportProperties.java @@ -1,8 +1,15 @@ package com.netgrif.application.engine.configuration.properties; import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +@Slf4j @Data +@Component +@ConfigurationProperties(prefix = "nae.case.export") public class CaseExportProperties { + private String fileName; -} +} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java b/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java deleted file mode 100644 index 0ccfed88cb8..00000000000 --- a/src/main/java/com/netgrif/application/engine/configuration/properties/CaseImportExportProperties.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.netgrif.application.engine.configuration.properties; - -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Slf4j -@Data -@Component -@ConfigurationProperties(prefix = "nae.case") -public class CaseImportExportProperties { - - private CaseExportProperties export; -} \ No newline at end of file diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java index 99c348a2278..0b235720b4d 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/CaseExportFiles.java @@ -1,28 +1,25 @@ package com.netgrif.application.engine.workflow.domain; -import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; -import org.apache.commons.lang3.tuple.ImmutablePair; - import java.util.*; public class CaseExportFiles { - private final Map, Set>>> caseFileMapping = new HashMap<>(); + private final Map> caseFileMapping = new HashMap<>(); - public void addFieldFilenames(String caseId, StorageField storageField, Set filenames) { - List, Set>> emptyFieldMapping = new ArrayList<>(); - List, Set>> fieldMapping = caseFileMapping.putIfAbsent(caseId, emptyFieldMapping); + public void addFieldFilenames(String caseId, StorageFieldWithFileNames storageField) { + List emptyFieldMapping = new ArrayList<>(); + List fieldMapping = caseFileMapping.putIfAbsent(caseId, emptyFieldMapping); if (fieldMapping == null) { fieldMapping = emptyFieldMapping; } - fieldMapping.add(new ImmutablePair<>(storageField, filenames)); + fieldMapping.add(storageField); } public Set getCaseIds() { return caseFileMapping.keySet(); } - public List, Set>> getFieldsOfCase(String caseId) { + public List getFieldsOfCase(String caseId) { return caseFileMapping.get(caseId); } } diff --git a/src/main/java/com/netgrif/application/engine/workflow/domain/StorageFieldWithFileNames.java b/src/main/java/com/netgrif/application/engine/workflow/domain/StorageFieldWithFileNames.java new file mode 100644 index 00000000000..1a7864156de --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/domain/StorageFieldWithFileNames.java @@ -0,0 +1,18 @@ +package com.netgrif.application.engine.workflow.domain; + +import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; +import lombok.Data; + +import java.util.Set; + +@Data +public class StorageFieldWithFileNames { + + private StorageField field; + private Set fileNames; + + public StorageFieldWithFileNames(StorageField field, Set fileNames) { + this.field = field; + this.fileNames = fileNames; + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java index ead0c83756f..b0493cf99ae 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseExporter.java @@ -15,7 +15,6 @@ import jakarta.xml.bind.Marshaller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import java.io.OutputStream; import java.math.BigInteger; @@ -48,8 +47,8 @@ public void exportCases(Collection { - this.caseToExport = caseToExport; + casesToExport.forEach(toExport -> { + this.caseToExport = toExport; this.translations = new HashMap<>(); xmlCases.getCase().add(exportCase()); }); @@ -57,11 +56,12 @@ public void exportCases(Collection options) { return null; } Options xmlOptions = objectFactory.createOptions(); - options.forEach( option -> { + options.forEach(option -> { Option xmlOption = objectFactory.createOption(); xmlOption.setName(option.getKey()); xmlOption.setValue(option.getDefaultValue()); @@ -340,7 +340,7 @@ private Properties exportProperties(Map properties, List { + optionIcons.forEach(icon -> { Icons xmlIcons = objectFactory.createIcons(); Icon xmlIcon = objectFactory.createIcon(); xmlIcon.setKey(icon.getKey()); @@ -360,7 +360,7 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { switch (type) { case DATE: case DATETIME: - LocalDateTime localDateTime = value instanceof Date ? convertDateToLocalDateTime((Date) value) : (value instanceof LocalDate ? ((LocalDate) value).atTime(LocalTime.NOON) : (LocalDateTime) value); + LocalDateTime localDateTime = value instanceof Date castValue ? convertDateToLocalDateTime(castValue) : (value instanceof LocalDate castLocalDateValue ? castLocalDateValue.atTime(LocalTime.NOON) : (LocalDateTime) value); values.getValue().add(exportLocalDateTime(localDateTime)); break; case CASE_REF: @@ -373,8 +373,8 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { case USER: case USERLIST: Set userFieldValues = new HashSet<>(); - if (value instanceof UserFieldValue) { - userFieldValues.add((UserFieldValue) value); + if (value instanceof UserFieldValue castValue) { + userFieldValues.add(castValue); } else { userFieldValues = ((UserListFieldValue) value).getUserValues(); } @@ -383,8 +383,8 @@ private StringCollection exportDataFieldValue(Object value, FieldType type) { case FILE: case FILELIST: Set fileFieldValues = new HashSet<>(); - if (value instanceof FileFieldValue) { - fileFieldValues.add((FileFieldValue) value); + if (value instanceof FileFieldValue castValue) { + fileFieldValues.add(castValue); } else { fileFieldValues = ((FileListFieldValue) value).getNamesPaths(); } diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java index 16ed579e292..f2dd15f421b 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java @@ -1,15 +1,15 @@ package com.netgrif.application.engine.workflow.service; import com.netgrif.application.engine.archive.interfaces.IArchiveService; -import com.netgrif.application.engine.configuration.properties.CaseImportExportProperties; +import com.netgrif.application.engine.configuration.properties.CaseExportProperties; import com.netgrif.application.engine.files.IStorageResolverService; import com.netgrif.application.engine.files.interfaces.IStorageService; import com.netgrif.application.engine.files.throwable.StorageException; -import com.netgrif.application.engine.petrinet.domain.dataset.*; -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.domain.CaseExportFiles; -import com.netgrif.application.engine.workflow.domain.DataField; -import com.netgrif.application.engine.workflow.domain.Task; +import com.netgrif.application.engine.petrinet.domain.dataset.FileFieldValue; +import com.netgrif.application.engine.petrinet.domain.dataset.FileListField; +import com.netgrif.application.engine.petrinet.domain.dataset.FileListFieldValue; +import com.netgrif.application.engine.petrinet.domain.dataset.StorageField; +import com.netgrif.application.engine.workflow.domain.*; import com.netgrif.application.engine.workflow.exceptions.ImportXmlFileMissingException; import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; @@ -37,7 +37,7 @@ public class CaseImportExportService implements ICaseImportExportService { private final IArchiveService archiveService; private final IWorkflowService workflowService; private final IStorageResolverService storageResolverService; - private final CaseImportExportProperties properties; + private final CaseExportProperties properties; private final ITaskService taskService; protected CaseImporter getCaseImporter() { @@ -60,7 +60,7 @@ public void findAndExportCasesWithFiles(Set caseIdsToExport, OutputStrea @Override public void exportCasesWithFiles(List casesToExport, OutputStream archiveStream) throws IOException { - File exportFile = new File(Files.createTempDirectory("case_export").toFile(), properties.getExport().getFileName()); + File exportFile = new File(Files.createTempDirectory("case_export").toFile(), properties.getFileName()); this.exportCases(casesToExport, new FileOutputStream(exportFile)); CaseExportFiles caseFiles = this.getFileNamesOfCases(casesToExport); archiveService.pack(archiveStream, caseFiles, exportFile.getPath()); @@ -90,7 +90,7 @@ public CaseExportFiles getFileNamesOfCases(List casesToExport) { } else { namesPaths.add(((FileFieldValue) dataField.getValue()).getName()); } - filesToExport.addFieldFilenames(exportCase.getStringId(), (StorageField) field, namesPaths); + filesToExport.addFieldFilenames(exportCase.getStringId(), new StorageFieldWithFileNames((StorageField) field, namesPaths)); }); } return filesToExport; @@ -103,16 +103,16 @@ public List importCases(InputStream inputStream) { if (importedCases.isEmpty()) { return importedCases; } - return saveImportedObjects(importedCases,importer.getImportedTasksMap()); + return saveImportedObjects(importedCases, importer.getImportedTasksMap()); } @Override public List importCasesWithFiles(InputStream importZipStream) throws IOException, StorageException, ImportXmlFileMissingException { String directoryPath = archiveService.unpack(importZipStream, Files.createTempDirectory(UUID.randomUUID().toString()).toString()); importZipStream.close(); - File caseExportXmlFile = FileUtils.getFile(new File(directoryPath.concat(File.separator).concat(properties.getExport().getFileName()))); + File caseExportXmlFile = FileUtils.getFile(new File(directoryPath.concat(File.separator).concat(properties.getFileName()))); if (!caseExportXmlFile.exists() || caseExportXmlFile.isDirectory()) { - throw new ImportXmlFileMissingException("Xml import file with name [" + properties.getExport().getFileName() + "] not found in archive"); + throw new ImportXmlFileMissingException("Xml import file with name [" + properties.getFileName() + "] not found in archive"); } FileInputStream fis = new FileInputStream(caseExportXmlFile); CaseImporter importer = getCaseImporter(); @@ -121,7 +121,7 @@ public List importCasesWithFiles(InputStream importZipStream) throws IOExc if (importedCases.isEmpty()) { return importedCases; } - importedCases = saveImportedObjects(importedCases,importer.getImportedTasksMap()); + importedCases = saveImportedObjects(importedCases, importer.getImportedTasksMap()); List caseFilesDirectories = Arrays.stream(Objects.requireNonNull(new File(directoryPath).list(DirectoryFileFilter.DIRECTORY))) .map(caseDirectory -> directoryPath.concat(File.separator).concat(caseDirectory)) .toList(); diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseExportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseExportController.java new file mode 100644 index 00000000000..a0c62be2098 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/web/CaseExportController.java @@ -0,0 +1,57 @@ +package com.netgrif.application.engine.workflow.web; + +import com.netgrif.application.engine.configuration.properties.CaseExportProperties; +import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Set; + +@Slf4j +@RestController() +@RequestMapping("/api/case/export") +@AllArgsConstructor +public class CaseExportController { + + private final ICaseImportExportService caseExportImportService; + private final CaseExportProperties properties; + + @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) + @GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + public ResponseEntity exportCases(@RequestParam("caseIds") Set caseIds, @RequestParam boolean withFiles) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + exportCases(caseIds, outputStream, withFiles); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); + String fileName = withFiles ? "case_export.zip" : properties.getFileName(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\""); + return ResponseEntity + .ok() + .headers(headers) + .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); + } + + private void exportCases(Set caseIds, ByteArrayOutputStream outputStream, boolean withFiles) { + try { + if (withFiles) { + caseExportImportService.findAndExportCasesWithFiles(caseIds, outputStream); + } else { + caseExportImportService.findAndExportCases(caseIds, outputStream); + } + } catch (IOException e) { + log.error("Error occurred during exporting of cases", e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportController.java new file mode 100644 index 00000000000..a97e8b20431 --- /dev/null +++ b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportController.java @@ -0,0 +1,70 @@ +package com.netgrif.application.engine.workflow.web; + +import com.netgrif.application.engine.files.throwable.StorageException; +import com.netgrif.application.engine.workflow.domain.Case; +import com.netgrif.application.engine.workflow.exceptions.ImportXmlFileMissingException; +import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RestController() +@RequestMapping("/api/case/import") +@AllArgsConstructor +public class CaseImportController { + + private final ICaseImportExportService caseExportImportService; + + @Operation(summary = "Import cases from xml file", security = {@SecurityRequirement(name = "BasicAuth")}) + @PostMapping(produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity importCases(@RequestPart(value = "file") MultipartFile multipartFile) { + List importedCases; + try { + importedCases = caseExportImportService.importCases(multipartFile.getInputStream()); + } catch (IOException e) { + log.error("Error occurred during importing of cases", e); + throw new RuntimeException(e); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return ResponseEntity + .ok() + .headers(headers) + .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); + } + + @Operation(summary = "Import cases from zip archive", security = {@SecurityRequirement(name = "BasicAuth")}) + @PostMapping(value = "/withFiles", produces = MediaType.TEXT_PLAIN_VALUE) + public ResponseEntity importCasesWithFiles(@RequestPart(value = "zipFile") MultipartFile multipartZipFile) { + List importedCases; + try { + importedCases = caseExportImportService.importCasesWithFiles(multipartZipFile.getInputStream()); + } catch (IOException | StorageException e) { + log.error("Error occurred during importing of cases", e); + throw new RuntimeException(e); + } catch (ImportXmlFileMissingException e) { + log.error("Xml file with cases to import missing from archive file", e); + return ResponseEntity.badRequest().body(e.getMessage()); + } + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); + return ResponseEntity + .ok() + .headers(headers) + .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); + } +} diff --git a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java b/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java deleted file mode 100644 index 6b8ba093712..00000000000 --- a/src/main/java/com/netgrif/application/engine/workflow/web/CaseImportExportController.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.netgrif.application.engine.workflow.web; - -import com.netgrif.application.engine.configuration.properties.CaseImportExportProperties; -import com.netgrif.application.engine.files.throwable.StorageException; -import com.netgrif.application.engine.workflow.domain.Case; -import com.netgrif.application.engine.workflow.exceptions.ImportXmlFileMissingException; -import com.netgrif.application.engine.workflow.service.interfaces.ICaseImportExportService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j -@RestController() -@RequestMapping("/api/case") -@AllArgsConstructor -public class CaseImportExportController { - - private final ICaseImportExportService caseExportImportService; - private final CaseImportExportProperties properties; - - @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) - @GetMapping(value = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity exportCases(@RequestParam("caseIds") Set caseIds) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try { - caseExportImportService.findAndExportCases(caseIds, outputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + properties.getExport().getFileName() + "\""); - return ResponseEntity - .ok() - .headers(headers) - .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); - } - - @Operation(summary = "Download xml file containing data of requested cases", security = {@SecurityRequirement(name = "BasicAuth")}) - @GetMapping(value = "/exportWithFiles", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) - public ResponseEntity exportCasesWithFiles(@RequestParam("caseIds") Set caseIds) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try { - caseExportImportService.findAndExportCasesWithFiles(caseIds, outputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"case_export.zip\""); - return ResponseEntity - .ok() - .headers(headers) - .body(new InputStreamResource(new ByteArrayInputStream(outputStream.toByteArray()))); - } - - @Operation(summary = "Import cases from xml file", security = {@SecurityRequirement(name = "BasicAuth")}) - @PostMapping(value = "/import", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity importCases(@RequestPart(value = "file") MultipartFile multipartFile) { - List importedCases; - try { - importedCases = caseExportImportService.importCases(multipartFile.getInputStream()); - } catch (IOException e) { - throw new RuntimeException(e); - } - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); - return ResponseEntity - .ok() - .headers(headers) - .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); - } - - @Operation(summary = "Import cases from zip archive", security = {@SecurityRequirement(name = "BasicAuth")}) - @PostMapping(value = "/importWithFiles", produces = MediaType.TEXT_PLAIN_VALUE) - public ResponseEntity importCasesWithFiles(@RequestPart(value = "zipFile") MultipartFile multipartZipFile) { - List importedCases; - try { - importedCases = caseExportImportService.importCasesWithFiles(multipartZipFile.getInputStream()); - } catch (IOException | StorageException e) { - throw new RuntimeException(e); - } catch (ImportXmlFileMissingException e) { - return ResponseEntity.badRequest().body(e.getMessage()); - } - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE); - return ResponseEntity - .ok() - .headers(headers) - .body(importedCases.stream().map(Case::getStringId).collect(Collectors.joining(","))); - } -} From 4dbf102e9d419564d9edf6453b82b45a1ec724e7 Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Wed, 30 Apr 2025 11:45:39 +0200 Subject: [PATCH 9/9] [NAE-1843] Import/Export services - refactor of importCasesWithFiles method to get rid of redundant database calls in saveFiles --- .../service/CaseImportExportService.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java index f2dd15f421b..1e54f1249b3 100644 --- a/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java +++ b/src/main/java/com/netgrif/application/engine/workflow/service/CaseImportExportService.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; +import java.util.stream.Collectors; @Service @Slf4j @@ -103,7 +104,7 @@ public List importCases(InputStream inputStream) { if (importedCases.isEmpty()) { return importedCases; } - return saveImportedObjects(importedCases, importer.getImportedTasksMap()); + return saveImportedObjects(importedCases, importer.getImportedTasksMap()).values().stream().toList(); } @Override @@ -121,19 +122,22 @@ public List importCasesWithFiles(InputStream importZipStream) throws IOExc if (importedCases.isEmpty()) { return importedCases; } - importedCases = saveImportedObjects(importedCases, importer.getImportedTasksMap()); + Map importedCasesMap = saveImportedObjects(importedCases, importer.getImportedTasksMap()); List caseFilesDirectories = Arrays.stream(Objects.requireNonNull(new File(directoryPath).list(DirectoryFileFilter.DIRECTORY))) .map(caseDirectory -> directoryPath.concat(File.separator).concat(caseDirectory)) .toList(); - saveFiles(caseFilesDirectories, importer.getImportedIdsMapping()); + Map idsToCaseMapping = new HashMap<>(); + importer.getImportedIdsMapping().forEach((oldCaseId, newCaseId) -> { + idsToCaseMapping.put(oldCaseId, importedCasesMap.get(newCaseId)); + }); + saveFiles(caseFilesDirectories, idsToCaseMapping); FileUtils.forceDelete(caseExportXmlFile.getParentFile()); return importedCases; } - private void saveFiles(List casesDirectories, Map importedIdsMapping) throws IOException, StorageException { + private void saveFiles(List casesDirectories, Map importedIdsMapping) throws IOException, StorageException { for (String caseDirectory : casesDirectories) { - String importedCaseId = importedIdsMapping.get(Paths.get(caseDirectory).getFileName().toString()); - Case importedCase = workflowService.findOne(importedCaseId); + Case importedCase = importedIdsMapping.get(Paths.get(caseDirectory).getFileName().toString()); List fieldsOfCaseDirectories = Arrays.stream(Objects.requireNonNull(new File(caseDirectory).list(DirectoryFileFilter.DIRECTORY))).toList(); for (String fieldDirectory : fieldsOfCaseDirectories) { String fieldDirectoryPath = caseDirectory.concat(File.separator).concat(fieldDirectory); @@ -150,11 +154,11 @@ private void saveFiles(List casesDirectories, Map import } } - private List saveImportedObjects(List importedCases, Map> importedTasks) { + private Map saveImportedObjects(List importedCases, Map> importedTasks) { return importedCases.stream().map(importedCase -> { taskService.save(importedTasks.get(importedCase.getStringId())); return workflowService.save(importedCase); - }).toList(); + }).collect(Collectors.toMap(Case::getStringId, c -> c)); } private void exportCasesToFile(Collection casesToExport, OutputStream exportFile) {