diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy index 1e686358e8..5ff63dcfae 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy @@ -1586,7 +1586,7 @@ class ActionDelegate { } FileFieldInputStream getFileFieldStream(Case useCase, Task task, FileField field, boolean forPreview = false) { - return this.dataService.getFile(useCase, task, field, forPreview) + return this.dataService.getFile(useCase, field, forPreview) } /** diff --git a/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java index e96009961a..e53d436208 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/actions/ActionApiImpl.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.netgrif.application.engine.adapter.spring.actions.ActionApi; +import com.netgrif.application.engine.adapter.spring.actions.ActionFileHolder; import com.netgrif.application.engine.auth.service.UserService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticCaseService; import com.netgrif.application.engine.elastic.service.interfaces.IElasticTaskService; @@ -27,16 +28,21 @@ import com.netgrif.application.engine.workflow.params.CreateCaseParams; import com.netgrif.application.engine.workflow.params.DeleteCaseParams; import com.netgrif.application.engine.workflow.params.TaskParams; +import com.netgrif.application.engine.workflow.service.FileFieldInputStream; import com.netgrif.application.engine.workflow.service.interfaces.IDataService; import com.netgrif.application.engine.workflow.service.interfaces.ITaskService; import com.netgrif.application.engine.workflow.service.interfaces.IWorkflowService; import com.querydsl.core.types.Predicate; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; +import java.io.*; import java.util.*; @Slf4j @@ -86,42 +92,51 @@ public void setElasticTaskService(IElasticTaskService elasticTaskService) { @Override public GetDataEventOutcome getData(String taskId, Map params) { + log.debug("Getting data for task [{}] with params: [{}]", taskId, params == null ? "null" : params.toString()); return dataService.getData(taskId, params); } @Override public SetDataEventOutcome setData(String taskId, Map> dataSet, Map params) throws JsonProcessingException { + log.debug("Setting data for task [{}] with params: [{}]", taskId, params == null ? "null" : params.toString()); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(dataSet); ObjectNode values = (ObjectNode) mapper.readTree(json); + log.trace("Setting data for task [{}] with params: [{}], values: [{}]", taskId, params == null ? "null" : params.toString(), values.toString()); return dataService.setData(taskId, values, params); } @Override public Page searchCases(String processIdentifier, Predicate predicate, Pageable pageable) { + log.debug("Searching cases for process identifier [{}] with predicate [{}], pageable [{}]", processIdentifier, predicate, pageable); return workflowService.search(predicate, pageable); } @Override public Page searchCases(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection) { + log.debug("Searching cases for elastic queries [{}] with auth principal [{}], pageable [{}], intersect [{}]", elasticStringQueries, authPrincipalDto, pageable, isIntersection); boolean intersect = Boolean.TRUE.equals(isIntersection); List caseSearchRequests = elasticStringQueries.stream().map(query -> CaseSearchRequest.builder().query(query).build()).toList(); LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); Locale locale = LocaleContextHolder.getLocale(); + log.trace("Searching cases for elastic queries [{}] with auth principal [{}], pageable [{}], intersect [{}], locale [{}]", elasticStringQueries, authPrincipalDto, pageable, isIntersection, locale); return elasticCaseService.search(caseSearchRequests, loggedUser, pageable, locale, intersect); } @Override public Long countCases(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Boolean isIntersection) { + log.debug("Counting cases for elastic queries [{}] with auth principal [{}], intersect [{}]", elasticStringQueries, authPrincipalDto, isIntersection); boolean intersect = Boolean.TRUE.equals(isIntersection); List caseSearchRequests = elasticStringQueries.stream().map(query -> CaseSearchRequest.builder().query(query).build()).toList(); LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); Locale locale = LocaleContextHolder.getLocale(); + log.trace("Counting cases for elastic queries [{}] with auth principal [{}], intersect [{}], locale [{}]", elasticStringQueries, authPrincipalDto, isIntersection, locale); return elasticCaseService.count(caseSearchRequests, loggedUser, locale, intersect); } @Override public CreateCaseEventOutcome createCaseByIdentifier(String identifier, String title, String color, AuthPrincipalDto authPrincipalDto, Map params) { + log.debug("Creating case with identifier [{}] and title [{}] and color [{}] with auth principal [{}] and params [{}]", identifier, title, color, authPrincipalDto, params); Locale locale = LocaleContextHolder.getLocale(); return workflowService.createCase(CreateCaseParams.with() .processIdentifier(identifier) @@ -135,6 +150,7 @@ public CreateCaseEventOutcome createCaseByIdentifier(String identifier, String t @Override public DeleteCaseEventOutcome deleteCase(String caseId, Map params) { + log.debug("Deleting case with id [{}] and params [{}]", caseId, params); return workflowService.deleteCase(DeleteCaseParams.with() .useCaseId(caseId) .params(params) @@ -143,20 +159,24 @@ public DeleteCaseEventOutcome deleteCase(String caseId, Map para @Override public Page searchTasks(String processIdentifier, Predicate predicate, Pageable pageable) { + log.debug("Searching tasks for process identifier [{}] with predicate [{}], pageable [{}]", processIdentifier, predicate, pageable); return taskService.search(predicate, pageable); } @Override public Page searchTasks(List elasticStringQueries, AuthPrincipalDto authPrincipalDto, Pageable pageable, Boolean isIntersection) { + log.debug("Searching tasks for elastic queries [{}] with auth principal [{}], pageable [{}], intersect [{}]", elasticStringQueries, authPrincipalDto, pageable, isIntersection); boolean intersect = Boolean.TRUE.equals(isIntersection); List taskSearchRequests = elasticStringQueries.stream().map(query -> ElasticTaskSearchRequest.builder().query(query).build()).toList(); LoggedUser loggedUser = ActorTransformer.toLoggedUser(resolveAbstractUser(authPrincipalDto)); Locale locale = LocaleContextHolder.getLocale(); + log.trace("Searching tasks for elastic queries [{}] with auth principal [{}], pageable [{}], intersect [{}], locale [{}]", elasticStringQueries, authPrincipalDto, pageable, isIntersection, locale); return elasticTaskService.search(taskSearchRequests, loggedUser, pageable, locale, intersect); } @Override public AssignTaskEventOutcome assignTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException { + log.debug("Assigning task [{}] with auth principal [{}] and params [{}]", taskId, authPrincipalDto, params); Task task = taskService.findOne(taskId); AbstractUser user = resolveAbstractUser(authPrincipalDto); return taskService.assignTask(TaskParams.with() @@ -168,6 +188,7 @@ public AssignTaskEventOutcome assignTask(String taskId, AuthPrincipalDto authPri @Override public CancelTaskEventOutcome cancelTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) { + log.debug("Canceling task [{}] with auth principal [{}] and params [{}]", taskId, authPrincipalDto, params); Task task = taskService.findOne(taskId); AbstractUser user = resolveAbstractUser(authPrincipalDto); return taskService.cancelTask(TaskParams.with() @@ -179,6 +200,7 @@ public CancelTaskEventOutcome cancelTask(String taskId, AuthPrincipalDto authPri @Override public FinishTaskEventOutcome finishTask(String taskId, AuthPrincipalDto authPrincipalDto, Map params) throws TransitionNotExecutableException { + log.debug("Finishing task [{}] with auth principal [{}] and params [{}]", taskId, authPrincipalDto, params); Task task = taskService.findOne(taskId); AbstractUser user = resolveAbstractUser(authPrincipalDto); return taskService.finishTask(TaskParams.with() @@ -190,16 +212,19 @@ public FinishTaskEventOutcome finishTask(String taskId, AuthPrincipalDto authPri @Override public Case findCase(String caseId) { + log.debug("Finding case with id [{}]", caseId); return workflowService.findOne(caseId); } @Override public Task findTask(String taskId) { + log.debug("Finding task with id [{}]", taskId); return taskService.findOne(taskId); } @Override public Page searchUsers(Predicate predicate, Pageable pageable, String realmId) { + log.debug("Searching users with predicate [{}] and pageable [{}] and realm ID [{}]", predicate, pageable, realmId); return userService.search(predicate, pageable, realmId); } @@ -208,6 +233,62 @@ public AbstractUser getSystemUser() { return userService.getSystem(); } + @Override + public SetDataEventOutcome saveFile(String taskId, String fieldId, ActionFileHolder file, Map params) { + log.debug("Saving file [{}] for task [{}] and field [{}] with params [{}]", file.getFileName(), taskId, fieldId, params); + MultipartFile multipartFile = new MockMultipartFile(file.getFileName(), file.getFileName(), null, file.getFileContent()); + log.trace("Saving file [{}] for task [{}] and field [{}] with params [{}]", multipartFile.getOriginalFilename(), taskId, fieldId, params); + return dataService.saveFile(taskId, fieldId, multipartFile, params); + } + + @Override + public SetDataEventOutcome saveFiles(String taskId, String fieldId, ActionFileHolder[] files, Map params) { + log.debug("Saving files [{}] for task [{}] and field [{}] with params [{}]", files.length, taskId, fieldId, params); + MultipartFile[] multipartFiles = new MultipartFile[files.length]; + for (int i = 0; i < files.length; i++) { + multipartFiles[i] = new MockMultipartFile(files[i].getFileName(), files[i].getFileName(), null, files[i].getFileContent()); + log.trace("Saving file [{}] for task [{}] and field [{}] with params [{}]", multipartFiles[i].getOriginalFilename(), taskId, fieldId, params); + } + log.trace("Saving files [{}] for task [{}] and field [{}] with params [{}]", multipartFiles.length, taskId, fieldId, params); + return dataService.saveFiles(taskId, fieldId, multipartFiles, params); + } + + @Override + public SetDataEventOutcome deleteFile(String taskId, String fieldId, Map params) { + log.debug("Deleting file for task [{}] and field [{}] with params [{}]", taskId, fieldId, params); + return dataService.deleteFile(taskId, fieldId, params); + } + + @Override + public SetDataEventOutcome deleteFileByName(String taskId, String fieldId, String name, Map params) { + log.debug("Deleting file [{}] for task [{}] and field [{}] with params [{}]", name, taskId, fieldId, params); + return dataService.deleteFileByName(taskId, fieldId, name, params); + } + + @Override + public ActionFileHolder getFile(String caseId, String fieldId, Boolean forPreview, Map params) throws IOException { + log.debug("Getting file for case [{}] and field [{}] with preview [{}] and params [{}]", caseId, fieldId, forPreview, params); + FileFieldInputStream fileFieldInputStream = dataService.getFile(caseId, fieldId, forPreview, params); + try (InputStream inputStream = fileFieldInputStream.getInputStream()) { + return ActionFileHolder.builder() + .fileName(fileFieldInputStream.getFileName()) + .fileContent(IOUtils.toByteArray(inputStream)) + .build(); + } + } + + @Override + public ActionFileHolder getFileByCaseAndName(String caseId, String fieldId, String name, Map params) throws IOException { + log.debug("Getting file [{}] for case [{}] and field [{}] with params [{}]", name, caseId, fieldId, params); + FileFieldInputStream fileFieldInputStream = dataService.getFileByCaseAndName(caseId, fieldId, name, params); + try (InputStream inputStream = fileFieldInputStream.getInputStream()) { + return ActionFileHolder.builder() + .fileName(fileFieldInputStream.getFileName()) + .fileContent(IOUtils.toByteArray(inputStream)) + .build(); + } + } + private AbstractUser resolveAbstractUser(AuthPrincipalDto authPrincipalDto) { if (authPrincipalDto == null) { throw new IllegalArgumentException("AuthPrincipalDto cannot be null."); diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java index f7f629f5c2..cc2fd19944 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/DataService.java @@ -523,7 +523,7 @@ private void changeTaskRefBehavior(LocalisedField field, FieldBehavior behavior) public FileFieldInputStream getFileByTask(String taskId, String fieldId, boolean forPreview) throws FileNotFoundException { Task task = taskService.findOne(taskId); - FileFieldInputStream fileFieldInputStream = getFileByCase(task.getCaseId(), task, fieldId, forPreview); + FileFieldInputStream fileFieldInputStream = getFileByCase(task.getCaseId(), fieldId, forPreview); if (fileFieldInputStream == null || fileFieldInputStream.getInputStream() == null) throw new FileNotFoundException("File in field %s within task %s was not found!".formatted(fieldId, taskId)); @@ -543,10 +543,10 @@ public FileFieldInputStream getFileByTaskAndName(String taskId, String fieldId, } @Override - public FileFieldInputStream getFileByCase(String caseId, Task task, String fieldId, boolean forPreview) throws FileNotFoundException { + public FileFieldInputStream getFileByCase(String caseId, String fieldId, boolean forPreview) throws FileNotFoundException { Case useCase = workflowService.findOne(caseId); FileField field = (FileField) useCase.getPetriNet().getDataSet().get(fieldId); - return getFile(useCase, task, field, forPreview); + return getFile(useCase, field, forPreview); } @Override @@ -584,12 +584,19 @@ public FileFieldInputStream getFileByName(Case useCase, FileListField field, Str } @Override - public FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException { - return getFile(useCase, task, field, forPreview, new HashMap<>()); + public FileFieldInputStream getFile(Case useCase, FileField field, boolean forPreview) throws FileNotFoundException { + return getFile(useCase, field, forPreview, new HashMap<>()); } @Override - public FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview, Map params) throws FileNotFoundException { + public FileFieldInputStream getFile(String caseId, String fieldId, boolean forPreview, Map params) throws FileNotFoundException { + Case useCase = workflowService.findOne(caseId); + FileField field = (FileField) useCase.getPetriNet().getDataSet().get(fieldId); + return getFile(useCase, field, forPreview, params); + } + + @Override + public FileFieldInputStream getFile(Case useCase, FileField field, boolean forPreview, Map params) throws FileNotFoundException { runGetActionsFromFileField(field.getEvents(), useCase, params); if (useCase.getFieldValue(field.getStringId()) == null) { throw new FileNotFoundException("Field %s not found on case %s".formatted(field.getStringId(), useCase.getStringId())); diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java index 889e1c0ce4..df876e73ef 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/service/interfaces/IDataService.java @@ -4,7 +4,6 @@ import com.netgrif.application.engine.objects.petrinet.domain.dataset.Field; import com.netgrif.application.engine.objects.petrinet.domain.dataset.FileField; import com.netgrif.application.engine.objects.petrinet.domain.dataset.FileListField; -import com.netgrif.application.engine.objects.petrinet.domain.dataset.UserFieldValue; import com.netgrif.application.engine.files.throwable.StorageException; import com.netgrif.application.engine.objects.petrinet.domain.dataset.*; import com.netgrif.application.engine.objects.workflow.domain.Case; @@ -51,9 +50,11 @@ public interface IDataService { SetDataEventOutcome setData(Task task, ObjectNode values, Map params, boolean runStrict); - FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview) throws FileNotFoundException; + FileFieldInputStream getFile(Case useCase, FileField field, boolean forPreview) throws FileNotFoundException; - FileFieldInputStream getFile(Case useCase, Task task, FileField field, boolean forPreview, Map params) throws FileNotFoundException; + FileFieldInputStream getFile(String caseId, String fieldId, boolean forPreview, Map params) throws FileNotFoundException; + + FileFieldInputStream getFile(Case useCase, FileField field, boolean forPreview, Map params) throws FileNotFoundException; FileFieldInputStream getFileByName(Case useCase, FileListField field, String name) throws FileNotFoundException; @@ -71,7 +72,7 @@ public interface IDataService { FileFieldInputStream getFileByTaskAndName(String taskId, String fieldId, String name, Map params) throws FileNotFoundException; - FileFieldInputStream getFileByCase(String caseId, Task task, String fieldId, boolean forPreview) throws FileNotFoundException; + FileFieldInputStream getFileByCase(String caseId, String fieldId, boolean forPreview) throws FileNotFoundException; FileFieldInputStream getFileByCaseAndName(String caseId, String fieldId, String name) throws FileNotFoundException; diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java index 8b61331f2b..9d4615b06e 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/WorkflowController.java @@ -249,7 +249,7 @@ public EntityModel deleteCase(Authentication auth, @Pat @Operation(summary = "Download case file field value", security = {@SecurityRequirement(name = "BasicAuth")}) @GetMapping(value = "/case/{id}/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) public ResponseEntity getFile(@PathVariable("id") String caseId, @RequestParam("fieldId") String fieldId) throws FileNotFoundException { - FileFieldInputStream fileFieldInputStream = dataService.getFileByCase(caseId, null, fieldId, false); + FileFieldInputStream fileFieldInputStream = dataService.getFileByCase(caseId, fieldId, false); if (fileFieldInputStream.getInputStream() == null) throw new FileNotFoundException("File in field " + fieldId + " within case " + caseId + " was not found!"); diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java index b2e112a6b1..09db189894 100644 --- a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApi.java @@ -18,6 +18,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Map; import java.util.List; @@ -195,4 +197,16 @@ public interface ActionApi { * @return the system user */ AbstractUser getSystemUser(); + + SetDataEventOutcome saveFile(String taskId, String fieldId, ActionFileHolder file, Map params); + + SetDataEventOutcome saveFiles(String taskId, String fieldId, ActionFileHolder[] files, Map params); + + SetDataEventOutcome deleteFile(String taskId, String fieldId, Map params); + + SetDataEventOutcome deleteFileByName(String taskId, String fieldId, String name, Map params); + + ActionFileHolder getFile(String caseId, String fieldId, Boolean forPreview, Map params) throws IOException; + + ActionFileHolder getFileByCaseAndName(String caseId, String fieldId, String name, Map params) throws IOException; } diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java index 79b9d673d8..50973f139a 100644 --- a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionApiMethods.java @@ -19,7 +19,13 @@ public enum ActionApiMethods { CANCEL_TASK("cancelTask"), FINISH_TASK("finishTask"), SEARCH_USERS("searchUsers"), - GET_SYSTEM_USER("getSystemUser"); + GET_SYSTEM_USER("getSystemUser"), + SAVE_FILE("saveFile"), + SAVE_FILES("saveFiles"), + DELETE_FILE("deleteFile"), + DELETE_FILE_BY_NAME("deleteFileByName"), + GET_FILE("getFile"), + GET_FILE_BY_CASE_AND_NAME("getFileByCaseAndName"); private String methodName; diff --git a/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionFileHolder.java b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionFileHolder.java new file mode 100644 index 0000000000..41048eb858 --- /dev/null +++ b/nae-spring-core-adapter/src/main/java/com/netgrif/application/engine/adapter/spring/actions/ActionFileHolder.java @@ -0,0 +1,19 @@ +package com.netgrif.application.engine.adapter.spring.actions; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Builder +public class ActionFileHolder implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String fileName; + + private byte[] fileContent; +}