diff --git a/pom.xml b/pom.xml index efb9ef488c2fb4d2522e73bbf197b859acb28a89..9aa64c0411432c5f08aad33884f74a936995e88e 100644 --- a/pom.xml +++ b/pom.xml @@ -3,10 +3,9 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-parent</artifactId> - <version>2.4.5</version> - <relativePath/> <!-- lookup parent from repository --> + <groupId>it.inaf.ia2</groupId> + <artifactId>vospace-parent</artifactId> + <version>0.0.1-SNAPSHOT</version> </parent> <groupId>it.inaf.oats</groupId> <artifactId>vospace-rest</artifactId> @@ -18,134 +17,19 @@ <!-- File catalog repository directory --> <init_database_scripts_path>../../../vospace-file-catalog</init_database_scripts_path> <finalName>${project.artifactId}-${project.version}</finalName> - <zonky.postgres-binaries.version>12.5.0</zonky.postgres-binaries.version> </properties> <dependencies> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-jdbc</artifactId> - </dependency> - - <dependency> - <groupId>org.postgresql</groupId> - <artifactId>postgresql</artifactId> - <scope>runtime</scope> - </dependency> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> - </dependency> - - <!-- Jackson-JAXB compatibility --> - <dependency> - <groupId>com.fasterxml.jackson.module</groupId> - <artifactId>jackson-module-jaxb-annotations</artifactId> - </dependency> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-devtools</artifactId> - <scope>runtime</scope> - <optional>true</optional> - </dependency> - - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-test</artifactId> - <scope>test</scope> - <exclusions> - <exclusion> - <groupId>org.junit.vintage</groupId> - <artifactId>junit-vintage-engine</artifactId> - </exclusion> - </exclusions> - </dependency> - - <dependency> - <groupId>it.oats.inaf</groupId> - <artifactId>vospace-datamodel</artifactId> - <version>1.0-SNAPSHOT</version> - <exclusions> - <!-- Transitive dependency excluded to avoid duplicated dependency issues. - We want to use always the version provided by Spring Boot --> - <exclusion> - <groupId>com.fasterxml.jackson.module</groupId> - <artifactId>jackson-module-jaxb-annotations</artifactId> - </exclusion> - </exclusions> - </dependency> - <dependency> <groupId>it.inaf.ia2</groupId> - <artifactId>auth-lib</artifactId> - <version>2.0.0-SNAPSHOT</version> + <artifactId>vospace-parent-classes</artifactId> + <version>0.0.1-SNAPSHOT</version> </dependency> - <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> - - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-inline</artifactId> - </dependency> - - <!-- Embedded PostgreSQL: --> - <dependency> - <groupId>com.opentable.components</groupId> - <artifactId>otj-pg-embedded</artifactId> - <version>0.13.3</version> - <scope>test</scope> - </dependency> - </dependencies> - - <profiles> - <profile> - <id>platform-linux</id> - <activation> - <os> - <family>unix</family> - </os> - </activation> - <dependencies> - <dependency> - <groupId>io.zonky.test.postgres</groupId> - <artifactId>embedded-postgres-binaries-linux-amd64</artifactId> - <version>${zonky.postgres-binaries.version}</version> - <scope>test</scope> - </dependency> - </dependencies> - </profile> - <profile> - <id>platform-windows</id> - <activation> - <os> - <family>windows</family> - </os> - </activation> - <dependencies> - <dependency> - <groupId>io.zonky.test.postgres</groupId> - <artifactId>embedded-postgres-binaries-windows-amd64</artifactId> - <version>${zonky.postgres-binaries.version}</version> - <scope>test</scope> - </dependency> - </dependencies> - </profile> - </profiles> - - <repositories> - <repository> - <id>ia2-snapshots</id> - <name>your custom repo</name> - <url>http://repo.ia2.inaf.it/maven/repository/snapshots</url> - </repository> - </repositories> <build> <finalName>${finalName}</finalName> @@ -173,17 +57,9 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> - <plugin> - <artifactId>maven-surefire-plugin</artifactId> - <version>2.22.2</version> - <configuration> - <trimStackTrace>false</trimStackTrace> - </configuration> - </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> - <version>0.8.6</version> <executions> <execution> <goals> @@ -202,4 +78,12 @@ </plugins> </build> + <repositories> + <repository> + <id>ia2-snapshots</id> + <name>IA2 snapshot repository</name> + <url>http://repo.ia2.inaf.it/maven/repository/snapshots</url> + </repository> + </repositories> + </project> diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeService.java b/src/main/java/it/inaf/oats/vospace/CreateNodeService.java index 206a85e68115c5fc109f1692010987430f7a40cd..516f110643e7fdcb2e108527a740a938d6e61b72 100644 --- a/src/main/java/it/inaf/oats/vospace/CreateNodeService.java +++ b/src/main/java/it/inaf/oats/vospace/CreateNodeService.java @@ -70,7 +70,7 @@ public class CreateNodeService { } if (!NodeUtils.checkIfWritable(parentNode, principal.getName(), principal.getGroups())) { - throw new PermissionDeniedException(path); + throw PermissionDeniedException.forPath(path); } // Check if node creator property is set. If not set it according to @@ -87,7 +87,7 @@ public class CreateNodeService { } else { if (!creator.equals(principal.getName())) // maybe a more specific exception would be more appropriate? { - throw new PermissionDeniedException(path); + throw PermissionDeniedException.forPath(path); } } diff --git a/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java b/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java index 45faca5611607002dfc18f7757d2b5abe1a8b64b..6ca9f25a2392c39a5a1261f27a40098c987d8105 100644 --- a/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java @@ -55,7 +55,7 @@ public class DeleteNodeController extends BaseNodeController { if (pathComponents.isEmpty()) { // Manage root node - throw new PermissionDeniedException("root"); + throw PermissionDeniedException.forPath("/"); } else { @@ -71,8 +71,8 @@ public class DeleteNodeController extends BaseNodeController { } - if(!NodeUtils.checkIfWritable(toBeDeletedNode, principal.getName(), principal.getGroups())) { - throw new PermissionDeniedException(path); + if (!NodeUtils.checkIfWritable(toBeDeletedNode, principal.getName(), principal.getGroups())) { + throw PermissionDeniedException.forPath(path); } try { diff --git a/src/main/java/it/inaf/oats/vospace/ErrorController.java b/src/main/java/it/inaf/oats/vospace/ErrorController.java new file mode 100644 index 0000000000000000000000000000000000000000..138bd4a32688ff5c8545ac399e028e15383d4590 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/ErrorController.java @@ -0,0 +1,20 @@ +/* + * This file is part of vospace-rest + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.oats.vospace; + +import it.inaf.oats.vospace.exception.DefaultErrorController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ErrorController extends DefaultErrorController { + + @Autowired + public ErrorController(ErrorAttributes errorAttributes) { + super(errorAttributes); + } +} diff --git a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java index 55fe5798847809f491c49e845384c33a57e11588..ed3bf416d82480def75d3663a7bd2ffbbb1e7af3 100644 --- a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java +++ b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java @@ -7,6 +7,8 @@ package it.inaf.oats.vospace; import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.datamodel.Views; +import it.inaf.oats.vospace.exception.InvalidArgumentException; import java.io.OutputStream; import java.util.List; import java.util.stream.Collectors; @@ -39,8 +41,33 @@ public class FileServiceClient { public String startArchiveJob(Transfer transfer, String jobId) { - List<String> vosPaths = transfer.getTarget().stream() - .map(p -> p.substring("vos://".length() + authority.length())).collect(Collectors.toList()); + if (transfer.getTarget().size() != 1) { + throw new IllegalArgumentException("Target size is " + transfer.getTarget().size()); + } + + String target = transfer.getTarget().get(0) + .substring("vos://".length() + authority.length()); + + String viewUri = transfer.getView().getUri(); + + // Generate list of paths using view include parameters + List<String> vosPaths = transfer.getView().getParam().stream() + .map(p -> { + if (p.getUri().equals(viewUri + "/include")) { + if (p.getValue().contains("../")) { + throw new InvalidArgumentException("Relative paths are not supported"); + } + return target + "/" + p.getValue(); + } else { + throw new InvalidArgumentException("Unsupported view parameter: " + p.getUri()); + } + }) + .collect(Collectors.toList()); + + if (vosPaths.isEmpty()) { + // Add target path + vosPaths.add(target); + } ArchiveRequest archiveRequest = new ArchiveRequest(); archiveRequest.setJobId(jobId); @@ -65,7 +92,7 @@ public class FileServiceClient { }, new Object[]{}); } - private static class ArchiveRequest { + public static class ArchiveRequest { private String type; private String jobId; @@ -98,9 +125,9 @@ public class FileServiceClient { private static String archiveTypeFromViewUri(String viewUri) { switch (viewUri) { - case "ivo://ia2.inaf.it/vospace/views#tar": + case Views.TAR_VIEW_URI: return "TAR"; - case "ivo://ia2.inaf.it/vospace/views#zip": + case Views.ZIP_VIEW_URI: return "ZIP"; default: throw new IllegalArgumentException("Archive type not defined for " + viewUri); diff --git a/src/main/java/it/inaf/oats/vospace/JobService.java b/src/main/java/it/inaf/oats/vospace/JobService.java index e74b84fafe775fef27778a839d63a4e034041772..15abb442b8181f9da0e44cc1a7f27915a69a8758 100644 --- a/src/main/java/it/inaf/oats/vospace/JobService.java +++ b/src/main/java/it/inaf/oats/vospace/JobService.java @@ -18,8 +18,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import it.inaf.oats.vospace.exception.VoSpaceErrorSummarizableException; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.uws.v1.ResultReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,9 +41,6 @@ public class JobService { @Autowired private CopyService copyService; - @Autowired - private NodeBranchService nodeBranchService; - @Autowired private AsyncTransferService asyncTransfService; @@ -105,16 +103,19 @@ public class JobService { phase = ExecutionPhase.EXECUTING; } job.setPhase(phase); + + jobDAO.updateJob(job, null); - jobDAO.updateJob(job); + Transfer negotiatedTransfer = null; switch (getJobDirection(transfer)) { case pullToVoSpace: - handlePullToVoSpace(job, transfer); + negotiatedTransfer = handlePullToVoSpace(job, transfer); break; case pullFromVoSpace: case pushToVoSpace: - handleVoSpaceUrlsListResult(job, transfer); + negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); + setJobResults(job, transfer); break; case moveNode: handleMoveNode(job, transfer); @@ -130,16 +131,18 @@ public class JobService { // the previous job are asynchronous. Each job has to set its // completion independently. Only jobs started from the /synctrans // endpoints are completed immediately (see createSyncJobResult() method) + + return negotiatedTransfer; }); } - private void handlePullToVoSpace(JobSummary job, Transfer transfer) { + private Transfer handlePullToVoSpace(JobSummary job, Transfer transfer) { for (Protocol protocol : transfer.getProtocols()) { switch (protocol.getUri()) { case "ia2:async-recall": asyncTransfService.startJob(job); - return; + return transfer; case "ivo://ivoa.net/vospace/core#httpget": if (transfer.getTarget().size() != 1) { throw new InvalidArgumentException("Invalid target size for pullToVoSpace: " + transfer.getTarget().size()); @@ -147,19 +150,18 @@ public class JobService { String nodeUri = transfer.getTarget().get(0); String contentUri = protocol.getEndpoint(); uriService.setNodeRemoteLocation(nodeUri, contentUri); - uriService.setTransferJobResult(job, transfer); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); + setJobResults(job, transfer); // Special case: import of a node from a portal file server // doesn't imply file transfer, so it can be set to completed job.setPhase(ExecutionPhase.COMPLETED); - return; + return negotiatedTransfer; default: - throw new InternalFaultException("Unsupported pullToVoSpace protocol: " + protocol.getUri()); + throw new InvalidArgumentException("Unsupported pullToVoSpace protocol: " + protocol.getUri()); } } - } - private void handleVoSpaceUrlsListResult(JobSummary job, Transfer transfer) { - uriService.setTransferJobResult(job, transfer); + throw new InvalidArgumentException("Transfer contains no protocols"); } private void handleMoveNode(JobSummary jobSummary, Transfer transfer) { @@ -170,6 +172,7 @@ public class JobService { handleJobErrors(jobSummary, job -> { moveService.processMoveJob(transfer, user); job.setPhase(ExecutionPhase.COMPLETED); + return null; }); }); } @@ -185,13 +188,16 @@ public class JobService { // the file service part will unlock nodes and set job phase // to completed + + return null; }); }); } - private void handleJobErrors(JobSummary job, Consumer<JobSummary> jobConsumer) { + private void handleJobErrors(JobSummary job, Function<JobSummary, Transfer> jobConsumer) { + Transfer negotiatedTransfer = null; try { - jobConsumer.accept(job); + negotiatedTransfer = jobConsumer.apply(job); } catch (VoSpaceErrorSummarizableException e) { job.setPhase(ExecutionPhase.ERROR); job.setErrorSummary(ErrorSummaryFactory.newErrorSummary(e)); @@ -200,7 +206,7 @@ public class JobService { job.setErrorSummary(ErrorSummaryFactory.newErrorSummary( new InternalFaultException(e))); } finally { - jobDAO.updateJob(job); + jobDAO.updateJob(job, negotiatedTransfer); } } @@ -217,21 +223,51 @@ public class JobService { * */ public void createSyncJobResult(JobSummary job) { + Transfer negotiatedTransfer = null; try { - uriService.setSyncTransferEndpoints(job); + Transfer transfer = uriService.getTransfer(job); + negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); + setJobResults(job, transfer); job.setPhase(ExecutionPhase.COMPLETED); // Need to catch other exceptions too to avoid inconsistent job status } catch (VoSpaceErrorSummarizableException e) { job.setPhase(ExecutionPhase.ERROR); - uriService.getTransfer(job).getProtocols().clear(); + stripProtocols(job, negotiatedTransfer); job.setErrorSummary(ErrorSummaryFactory.newErrorSummary(e)); } catch (Exception e) { job.setPhase(ExecutionPhase.ERROR); - uriService.getTransfer(job).getProtocols().clear(); + stripProtocols(job, negotiatedTransfer); job.setErrorSummary(ErrorSummaryFactory.newErrorSummary( new InternalFaultException(e))); } finally { - jobDAO.createJob(job); + jobDAO.createJob(job, negotiatedTransfer); + } + } + + private void stripProtocols(JobSummary job, Transfer negotiatedTransfer) { + uriService.getTransfer(job).getProtocols().clear(); + if (negotiatedTransfer != null) { + negotiatedTransfer.getProtocols().clear(); + } + } + + private void setJobResults(JobSummary jobSummary, Transfer transfer) { + String baseUrl = servletRequest.getRequestURL().substring(0, + servletRequest.getRequestURL().indexOf(servletRequest.getContextPath())); + String href = baseUrl + servletRequest.getContextPath() + + "/transfers/" + jobSummary.getJobId() + "/results/transferDetails"; + ResultReference transferDetailsRef = new ResultReference(); + transferDetailsRef.setId("transferDetails"); + transferDetailsRef.setHref(href); + jobSummary.getResults().add(transferDetailsRef); + switch (getJobDirection(transfer)) { + case pullFromVoSpace: + case pushToVoSpace: + ResultReference dataNodeRef = new ResultReference(); + dataNodeRef.setId("dataNode"); + dataNodeRef.setHref(transfer.getTarget().get(0)); + jobSummary.getResults().add(dataNodeRef); + break; } } } diff --git a/src/main/java/it/inaf/oats/vospace/ListNodeController.java b/src/main/java/it/inaf/oats/vospace/ListNodeController.java index bcc4b50a2407a435c47ed1d6f98dac34fb5ac5d5..1da35f704589be2336e568229bc25c7aca5e1dbc 100644 --- a/src/main/java/it/inaf/oats/vospace/ListNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/ListNodeController.java @@ -47,7 +47,7 @@ public class ListNodeController extends BaseNodeController { } else { if (!NodeUtils.checkIfReadable( optNode.get(), principal.getName(), principal.getGroups())) { - throw new PermissionDeniedException(path); + throw PermissionDeniedException.forPath(path); } } diff --git a/src/main/java/it/inaf/oats/vospace/MoveService.java b/src/main/java/it/inaf/oats/vospace/MoveService.java index 5ff1f24f0f153c950709792819c1918d7d9911b1..f83bcdc5fac7359111d7eb368f9937c0bcf7280d 100644 --- a/src/main/java/it/inaf/oats/vospace/MoveService.java +++ b/src/main/java/it/inaf/oats/vospace/MoveService.java @@ -7,6 +7,7 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.datamodel.NodeUtils; +import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.NodeNotFoundException; @@ -66,7 +67,7 @@ public class MoveService extends AbstractNodeService { } if (!nodeDao.isBranchWritable(sourceId, user.getName(), user.getGroups())) { - throw new PermissionDeniedException(sourcePath); + throw PermissionDeniedException.forPath(sourcePath); } Optional<ShortNodeDescriptor> destShortNodeDescriptor @@ -75,8 +76,13 @@ public class MoveService extends AbstractNodeService { String destinationNodeLtreePath = null; if (destShortNodeDescriptor.isPresent()) { // When the destination is an existing ContainerNode, the source SHALL be placed under it (i.e., within the container) - ShortNodeDescriptor snd = destShortNodeDescriptor.get(); - this.validateDestinationContainer(snd, destinationPath); + ShortNodeDescriptor snd = destShortNodeDescriptor.get(); + + if(snd.isBusy()) throw new NodeBusyException(destinationPath); + if(snd.isPermissionDenied()) throw PermissionDeniedException.forPath(destinationPath); + if(!snd.isWritable()) throw new InternalFaultException("Destination is not writable: "+ destinationPath); + if(!snd.isContainer()) throw new InternalFaultException("Existing destination is not a container: " + destinationPath); + destinationNodeLtreePath = snd.getDestinationNodeLtreePath(); } else { diff --git a/src/main/java/it/inaf/oats/vospace/SetNodeController.java b/src/main/java/it/inaf/oats/vospace/SetNodeController.java index 16de28d6b009e88aa5d57f22852154395e6fff45..91b6d7fa4ef332fe22fbd1534b7faee4d9e79ab1 100644 --- a/src/main/java/it/inaf/oats/vospace/SetNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/SetNodeController.java @@ -50,7 +50,7 @@ public class SetNodeController extends BaseNodeController { // The service SHALL throw a HTTP 403 status code including a PermissionDenied fault // in the entity-body if the user does not have permissions to perform the operation if (!NodeUtils.checkIfWritable(toBeModifiedNode, principal.getName(), principal.getGroups())) { - throw new PermissionDeniedException(path); + throw PermissionDeniedException.forPath(path); } // The service SHALL throw a HTTP 403 status code including a PermissionDenied fault @@ -60,7 +60,7 @@ public class SetNodeController extends BaseNodeController { String newNodeType = node.getType(); if (!storedNodeType.equals(newNodeType)) { LOG.debug("setNode trying to modify type. Stored ", storedNodeType + ", requested " + newNodeType); - throw new PermissionDeniedException(path); + throw PermissionDeniedException.forPath(path); } // This method cannot be used to modify the accepts or provides list of Views for the Node. diff --git a/src/main/java/it/inaf/oats/vospace/TransferController.java b/src/main/java/it/inaf/oats/vospace/TransferController.java index eda5ff0f1ea8fe9af09ac8685242c6baee924bd5..c7c2d512381c907e0f0e85c1909f271c6ca36b62 100644 --- a/src/main/java/it/inaf/oats/vospace/TransferController.java +++ b/src/main/java/it/inaf/oats/vospace/TransferController.java @@ -53,7 +53,7 @@ public class TransferController { JobSummary jobSummary = newJobSummary(transfer, principal); - jobDAO.createJob(jobSummary); + jobDAO.createJob(jobSummary, null); if (phase.isPresent()) { jobService.setJobPhase(jobSummary, phase.get()); @@ -157,8 +157,7 @@ public class TransferController { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - // TODO: check type - return ResponseEntity.ok((Transfer) (job.getJobInfo().getAny().get(0))); + return ResponseEntity.ok(jobDAO.getTransferDetails(jobId)); }).orElse(ResponseEntity.notFound().build()); } @@ -174,6 +173,7 @@ public class TransferController { @RequestParam(value = "PHASE", required = false) Optional<List<ExecutionPhase>> phase, @RequestParam(value = "AFTER", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Optional<LocalDateTime> after, @RequestParam(value = "LAST", required = false) Optional<Integer> last, + @RequestParam(value = "VIEW", required = false) Optional<List<String>> views, @RequestParam(value = "direction", required = false) Optional<List<JobService.JobDirection>> direction, User principal) { @@ -185,21 +185,11 @@ public class TransferController { String userId = principal.getName(); - List<ExecutionPhase> phaseList; - if (phase.isPresent()) { - phaseList = phase.get(); - } else { - phaseList = List.of(); - } - - List<JobService.JobDirection> directionList; - if (direction.isPresent()) { - directionList = direction.get(); - } else { - directionList = List.of(); - } + List<ExecutionPhase> phaseList = phase.orElse(List.of()); + List<JobService.JobDirection> directionList = direction.orElse(List.of()); + List<String> viewsList = views.orElse(List.of()); - Jobs jobs = jobDAO.getJobs(userId, phaseList, directionList, after, last); + Jobs jobs = jobDAO.getJobs(userId, phaseList, directionList, viewsList, after, last); return ResponseEntity.ok(jobs); } diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java index aa8db7b479e895e9fff4c0a87d2b22facf433940..06ed2b41cbf29ad73aeaa2c9484958f94356b749 100644 --- a/src/main/java/it/inaf/oats/vospace/UriService.java +++ b/src/main/java/it/inaf/oats/vospace/UriService.java @@ -31,7 +31,6 @@ import java.util.Optional; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; -import net.ivoa.xml.uws.v1.ResultReference; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Protocol; @@ -67,25 +66,18 @@ public class UriService { @Autowired private FileServiceClient fileServiceClient; - public void setTransferJobResult(JobSummary job, Transfer transfer) { - - List<ResultReference> results = new ArrayList<>(); - - ResultReference result = new ResultReference(); - result.setHref(getEndpoint(job, transfer)); - results.add(result); - - job.setResults(results); - // Moved phase setting to caller method for ERROR management - } - /** - * Sets the endpoint value for all valid protocols (protocol negotiation). + * For a given job, returns a new transfer object containing only valid + * protocols (protocol negotiation) and sets proper endpoints on them. */ - public void setSyncTransferEndpoints(JobSummary job) { - - Transfer transfer = getTransfer(job); - + public Transfer getNegotiatedTransfer(JobSummary job, Transfer transfer) { + + // Original transfer object shouldn't be modified, so a new transfer object is created + Transfer negotiatedTransfer = new Transfer(); + negotiatedTransfer.setTarget(transfer.getTarget()); + negotiatedTransfer.setDirection(transfer.getDirection()); + // according to examples found in specification view is not copied + if (transfer.getProtocols().isEmpty()) { // At least one protocol is expected from client throw new InvalidArgumentException("Transfer contains no protocols"); @@ -97,6 +89,7 @@ public class UriService { List<String> validProtocolUris = new ArrayList<>(); switch (jobDirection) { case pullFromVoSpace: + case pullToVoSpace: validProtocolUris.add("ivo://ivoa.net/vospace/core#httpget"); break; case pushToVoSpace: @@ -109,20 +102,24 @@ public class UriService { List<Protocol> validProtocols = transfer.getProtocols().stream() + // discard invalid protocols .filter(protocol -> validProtocolUris.contains(protocol.getUri())) - .collect(Collectors.toList()); + .map(p -> { + // set endpoints + Protocol protocol = new Protocol(); + protocol.setUri(p.getUri()); + protocol.setEndpoint(getEndpoint(job, transfer)); + return protocol; + }).collect(Collectors.toList()); if (validProtocols.isEmpty()) { Protocol protocol = transfer.getProtocols().get(0); throw new ProtocolNotSupportedException(protocol.getUri()); } - String endpoint = getEndpoint(job, transfer); - validProtocols.stream().forEach(p -> p.setEndpoint(endpoint)); - - // Returns modified transfer containing only valid protocols - transfer.getProtocols().clear(); - transfer.getProtocols().addAll(validProtocols); + negotiatedTransfer.getProtocols().addAll(validProtocols); + + return negotiatedTransfer; } private Node getEndpointNode(String relativePath, @@ -168,13 +165,13 @@ public class UriService { case pushToVoSpace: case pullToVoSpace: if (!NodeUtils.checkIfWritable(node, creator, groups)) { - throw new PermissionDeniedException(relativePath); + throw PermissionDeniedException.forPath(relativePath); } break; case pullFromVoSpace: if (!NodeUtils.checkIfReadable(node, creator, groups)) { - throw new PermissionDeniedException(relativePath); + throw PermissionDeniedException.forPath(relativePath); } break; diff --git a/src/main/java/it/inaf/oats/vospace/exception/ContainerNotFoundException.java b/src/main/java/it/inaf/oats/vospace/exception/ContainerNotFoundException.java deleted file mode 100644 index 95fdd32dd6ab812e2122ac2d009cd616569afc98..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/ContainerNotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.NOT_FOUND) -public class ContainerNotFoundException extends VoSpaceErrorSummarizableException { - - public ContainerNotFoundException(String path) { - super("Path: " + path, - VOSpaceFaultEnum.NODE_NOT_FOUND); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/DuplicateNodeException.java b/src/main/java/it/inaf/oats/vospace/exception/DuplicateNodeException.java deleted file mode 100644 index 95c0c241583bace74160127b2fdafa14beea9444..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/DuplicateNodeException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.CONFLICT) -public class DuplicateNodeException extends VoSpaceErrorSummarizableException { - - public DuplicateNodeException(String path) { - super("Path: " + path, - VOSpaceFaultEnum.DUPLICATE_NODE); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/ErrorController.java b/src/main/java/it/inaf/oats/vospace/exception/ErrorController.java deleted file mode 100644 index 17d2091445134e663d2312a23a4a9f785e4e8932..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/ErrorController.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import java.nio.charset.StandardCharsets; -import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("${server.error.path:${error.path:/error}}") -public class ErrorController extends AbstractErrorController { - - @Autowired - public ErrorController(ErrorAttributes errorAttributes) { - super(errorAttributes); - } - - @RequestMapping(produces = MediaType.TEXT_XML_VALUE) - public void errorText(HttpServletRequest request, HttpServletResponse response) throws Exception { - ErrorAttributeOptions options = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE); - Map<String, Object> errors = super.getErrorAttributes(request, options); - response.setContentType("text/plain;charset=UTF-8"); - response.setCharacterEncoding("UTF-8"); - String errorMessage = (String) errors.get("message"); - if (errorMessage != null) { - response.getOutputStream().write(errorMessage.getBytes(StandardCharsets.UTF_8)); - } - } - - @Override - public String getErrorPath() { - return null; - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/ErrorSummaryFactory.java b/src/main/java/it/inaf/oats/vospace/exception/ErrorSummaryFactory.java deleted file mode 100644 index 7db96116ffa38fe5075e9243396d5a075d7c77c3..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/ErrorSummaryFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import net.ivoa.xml.uws.v1.ErrorSummary; - -public class ErrorSummaryFactory { - - public static ErrorSummary newErrorSummary(VOSpaceFaultEnum error, String detailMessage) { - ErrorSummary result = new ErrorSummary(); - result.setMessage(error.getFaultRepresentation()); - result.setType(error.getType()); - - if (detailMessage == null || detailMessage.isBlank()) { - result.setHasDetail(false); - } else { - result.setHasDetail(true); - result.setDetailMessage(error.getFaultCaptionForDetails() - + " " - + detailMessage); - } - - return result; - } - - public static ErrorSummary newErrorSummary(VOSpaceFaultEnum error) { - return newErrorSummary(error, null); - } - - public static ErrorSummary newErrorSummary(VoSpaceErrorSummarizableException e) - { - return newErrorSummary(e.getFault(), e.getDetailMessage()); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java b/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java deleted file mode 100644 index 1e78a4fb1e90363e68f636995b81c1dbcc2b0c74..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) // Status code 500 -public class InternalFaultException extends VoSpaceErrorSummarizableException { - - private static final Logger LOG = LoggerFactory.getLogger(InternalFaultException.class); - - public InternalFaultException(String msg) { - super("Description: " + msg, - VOSpaceFaultEnum.INTERNAL_FAULT); - } - - public InternalFaultException(Throwable cause) { - super("Description: " + getMessage(cause), - VOSpaceFaultEnum.INTERNAL_FAULT); - } - - private static String getMessage(Throwable cause) { - LOG.error("Exception caught", cause); - return cause.getMessage() != null ? cause.getMessage() : cause.getClass().getCanonicalName(); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java b/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java deleted file mode 100644 index d3bf46ec98b40acd2f552f89143dc1c811a04302..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/InvalidArgumentException.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.BAD_REQUEST) -public class InvalidArgumentException extends VoSpaceErrorSummarizableException { - - public InvalidArgumentException(String message) { - super("Description: " + message, VOSpaceFaultEnum.NODE_NOT_FOUND); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/InvalidURIException.java b/src/main/java/it/inaf/oats/vospace/exception/InvalidURIException.java deleted file mode 100644 index 3c9248e6db690c17c1dd4be9cfaf8e49c5152200..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/InvalidURIException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.BAD_REQUEST) -public class InvalidURIException extends VoSpaceErrorSummarizableException { - - public InvalidURIException(String URI, String path) { - super("Payload node URI: " + URI - + " is not consistent with request path: " + path, - VOSpaceFaultEnum.INVALID_URI); - } - - public InvalidURIException(String URI) { - super("URI: " + URI + " is not in a valid format", - VOSpaceFaultEnum.INVALID_URI); - } - - public InvalidURIException(IllegalArgumentException ex) { - super("Description: " + ex.getMessage(), - VOSpaceFaultEnum.INVALID_URI); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/LinkFoundException.java b/src/main/java/it/inaf/oats/vospace/exception/LinkFoundException.java deleted file mode 100644 index 55d3f99cb0cf7b5dce9d8fd834c60f2d5d57c751..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/LinkFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.BAD_REQUEST) -public class LinkFoundException extends VoSpaceErrorSummarizableException { - - public LinkFoundException(String path) { - super("Link Node found at path: " + path, - VOSpaceFaultEnum.INVALID_URI); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/NodeBusyException.java b/src/main/java/it/inaf/oats/vospace/exception/NodeBusyException.java deleted file mode 100644 index 7e0d8fc3d58e538de3f56e5253f03ebf00fdf277..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/NodeBusyException.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -public class NodeBusyException extends VoSpaceErrorSummarizableException { - - public NodeBusyException(String path) { - super("Path: " + path, - VOSpaceFaultEnum.NODE_BUSY); - } - -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/NodeNotFoundException.java b/src/main/java/it/inaf/oats/vospace/exception/NodeNotFoundException.java deleted file mode 100644 index 9024dc2fe9b4cc0cf5d2a69715d9add1802b9ba5..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/NodeNotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.NOT_FOUND) -public class NodeNotFoundException extends VoSpaceErrorSummarizableException { - - public NodeNotFoundException(String path) { - super("Path: " + path, - VOSpaceFaultEnum.NODE_NOT_FOUND); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/PermissionDeniedException.java b/src/main/java/it/inaf/oats/vospace/exception/PermissionDeniedException.java deleted file mode 100644 index 446e410a900c53f210c514836b37942104b0df1e..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/PermissionDeniedException.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.FORBIDDEN) -public class PermissionDeniedException extends VoSpaceErrorSummarizableException { - - public PermissionDeniedException(String path) { - super("Path: " + path, - VOSpaceFaultEnum.PERMISSION_DENIED); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/ProtocolNotSupportedException.java b/src/main/java/it/inaf/oats/vospace/exception/ProtocolNotSupportedException.java deleted file mode 100644 index 6a767964c4bd9c5465d591f9663c1e0a450800fc..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/ProtocolNotSupportedException.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -public class ProtocolNotSupportedException extends VoSpaceErrorSummarizableException{ - - public ProtocolNotSupportedException(String protocol) { - super("Protocol: " + protocol, - VOSpaceFaultEnum.PROTOCOL_NOT_SUPPORTED); - } - -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/VOSpaceFaultEnum.java b/src/main/java/it/inaf/oats/vospace/exception/VOSpaceFaultEnum.java deleted file mode 100644 index 82bf0b5e27dd14d4b41555878cadc605dfd0800b..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/VOSpaceFaultEnum.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -// NFC: ErrorType usage is not covered in documentation, as far as I can see -// these are tentative default values. -import net.ivoa.xml.uws.v1.ErrorType; - -public enum VOSpaceFaultEnum { - // pushto - OPERATION_NOT_SUPPORTED("Operation Not Supported", ErrorType.FATAL, "OperationNotSupported"), - INTERNAL_FAULT("Internal Fault", ErrorType.TRANSIENT, "InternalFault"), - PERMISSION_DENIED("Permission Denied", ErrorType.FATAL, "PermissionDenied"), - VIEW_NOT_SUPPORTED("View Not Supported", ErrorType.FATAL, "ViewNotSupported"), - PROTOCOL_NOT_SUPPORTED("Protocol Not Supported", ErrorType.FATAL, "ProtocolNotSupported"), - INVALID_ARGUMENT("Invalid Argument", ErrorType.FATAL, "InvalidArgument"), - NODE_BUSY("Node Busy", ErrorType.TRANSIENT, "NodeBusy"), - // additional for pullto - INVALID_URI("Invalid URI", ErrorType.FATAL, "InvalidURI"), - INVALID_DATA("Invalid Data", ErrorType.FATAL, "InvalidData"), - // additional for pullfrom - NODE_NOT_FOUND("Node Not Found", ErrorType.FATAL, "NodeNotFound"), - // additional for pushfrom - TRANSFER_FAILED("Transfer Failed", ErrorType.FATAL, "TransferFailed"), - // additional for movenode/copynode - DUPLICATE_NODE("Duplicate Node", ErrorType.FATAL, "DuplicateNode"); - - private final String faultRepresentation; - private final ErrorType type; - private final String faultCaptionForDetails; - - private VOSpaceFaultEnum(String faultRepresentation, - ErrorType type, - String faultCaptionForDetails) { - this.faultRepresentation = faultRepresentation; - this.type = type; - this.faultCaptionForDetails = faultCaptionForDetails; - } - - public String getFaultRepresentation() { - return this.faultRepresentation; - } - - public ErrorType getType() { - return this.type; - } - - public String getFaultCaptionForDetails() { - return faultCaptionForDetails; - } - -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/VoSpaceErrorSummarizableException.java b/src/main/java/it/inaf/oats/vospace/exception/VoSpaceErrorSummarizableException.java deleted file mode 100644 index ebd8b2ab4a4b195ec34170c320ff3913cc63f6dc..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/VoSpaceErrorSummarizableException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) -public abstract class VoSpaceErrorSummarizableException extends VoSpaceException { - - VOSpaceFaultEnum fault; - private String detailMessage; - - public VoSpaceErrorSummarizableException(String detailMessage, VOSpaceFaultEnum fault) - { - super(fault.getFaultCaptionForDetails() + " " + detailMessage); - this.detailMessage = detailMessage; - this.fault = fault; - } - - public VOSpaceFaultEnum getFault() - { - return this.fault; - } - - public String getDetailMessage() - { - return this.detailMessage; - } -} diff --git a/src/main/java/it/inaf/oats/vospace/exception/VoSpaceException.java b/src/main/java/it/inaf/oats/vospace/exception/VoSpaceException.java deleted file mode 100644 index 4d3c983630f051f8a5b358df4399261e36b8f82a..0000000000000000000000000000000000000000 --- a/src/main/java/it/inaf/oats/vospace/exception/VoSpaceException.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This file is part of vospace-rest - * Copyright (C) 2021 Istituto Nazionale di Astrofisica - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.inaf.oats.vospace.exception; - -public class VoSpaceException extends RuntimeException { - - public VoSpaceException(String message) { - super(message); - } -} diff --git a/src/main/java/it/inaf/oats/vospace/persistence/JobDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/JobDAO.java index 03a9e05968ccfe371a310562b5ae1bbceaa39aed..e2b52369f65d270900f8ac0eed9642ed226f9516 100644 --- a/src/main/java/it/inaf/oats/vospace/persistence/JobDAO.java +++ b/src/main/java/it/inaf/oats/vospace/persistence/JobDAO.java @@ -31,6 +31,7 @@ import org.springframework.stereotype.Repository; import java.util.ArrayList; import java.time.LocalDateTime; import java.math.BigDecimal; +import java.util.Collections; import net.ivoa.xml.uws.v1.ErrorType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,12 +50,12 @@ public class JobDAO { jdbcTemplate = new JdbcTemplate(dataSource); } - public void createJob(JobSummary jobSummary) { + public void createJob(JobSummary jobSummary, Transfer transferDetails) { - String sql = - "INSERT INTO job(job_id, owner_id, job_type, phase, job_info," - + " error_message, error_type, error_has_detail, error_detail) " - + "VALUES (?, ?, ?, ?, ?, ? ,? ,? ,?)"; + String sql + = "INSERT INTO job(job_id, owner_id, job_type, phase, job_info, transfer_details, " + + " results, error_message, error_type, error_has_detail, error_detail) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; jdbcTemplate.update(sql, ps -> { int i = 0; @@ -63,9 +64,11 @@ public class JobDAO { ps.setObject(++i, getJobDirection(jobSummary), Types.VARCHAR); ps.setObject(++i, jobSummary.getPhase().value(), Types.OTHER); ps.setObject(++i, toJson(jobSummary.getJobInfo()), Types.OTHER); - + ps.setObject(++i, toJson(transferDetails), Types.OTHER); + ps.setObject(++i, toJson(jobSummary.getResults()), Types.OTHER); + ErrorSummary errorSummary = jobSummary.getErrorSummary(); - if(errorSummary != null) { + if (errorSummary != null) { ps.setString(++i, errorSummary.getMessage()); ps.setObject(++i, errorSummary.getType().value(), Types.OTHER); ps.setBoolean(++i, errorSummary.isHasDetail()); @@ -125,7 +128,10 @@ public class JobDAO { jobSummary.setPhase(ExecutionPhase.fromValue(rs.getString("phase"))); jobSummary.setJobInfo(getJobPayload(rs.getString("job_info"))); jobSummary.setResults(getResults(rs.getString("results"))); - + jobSummary.setCreationTime(toXMLGregorianCalendar(rs.getTimestamp("creation_time"))); + jobSummary.setStartTime(toXMLGregorianCalendar(rs.getTimestamp("start_time"))); + jobSummary.setEndTime(toXMLGregorianCalendar(rs.getTimestamp("end_time"))); + // Retrieve error information if any String errorType = rs.getString("error_type"); if (errorType != null) { @@ -137,13 +143,14 @@ public class JobDAO { jobSummary.setErrorSummary(errorSummary); } - + return jobSummary; } public Jobs getJobs(String userId, List<ExecutionPhase> phaseList, List<JobService.JobDirection> directionList, + List<String> viewList, Optional<LocalDateTime> after, Optional<Integer> last ) { @@ -193,6 +200,18 @@ public class JobDAO { } sb.append(")"); } + + // Fill conditions on views list + if (!viewList.isEmpty()) { + sb.append(" AND (") + .append(String.join(" OR ", + Collections.nCopies(viewList.size(), "job_info->'transfer'->'view'->>'uri' = ?"))) + .append(")"); + for (String view : viewList) { + queryParams.add(view); + queryParamTypes.add(Types.VARCHAR); + } + } // Fill conditions on creation date if (after.isPresent()) { @@ -260,41 +279,58 @@ public class JobDAO { } } - public void updateJob(JobSummary job) { + public void updateJob(JobSummary job, Transfer transferDetails) { + + String sql = "UPDATE job SET (phase, results, transfer_details "; - String sql = "UPDATE job SET (phase, results"; - ErrorSummary errorSummary = job.getErrorSummary(); - if(errorSummary != null) - { + if (errorSummary != null) { sql += ", error_message, error_type, error_has_detail, error_detail"; - } - - sql += ") = (?, ?"; - - if(errorSummary != null) - { + } + + sql += ") = (?, ?, ?"; + + if (errorSummary != null) { sql += ", ?, ?, ?, ?"; } - + sql += ") WHERE job_id = ?"; jdbcTemplate.update(sql, ps -> { int i = 0; ps.setObject(++i, job.getPhase().name(), Types.OTHER); ps.setObject(++i, toJson(job.getResults()), Types.OTHER); - if(errorSummary != null) - { + ps.setObject(++i, toJson(transferDetails), Types.OTHER); + if (errorSummary != null) { ps.setString(++i, errorSummary.getMessage()); - ps.setObject(++i, errorSummary.getType().value(), Types.OTHER); + ps.setObject(++i, errorSummary.getType().value(), Types.OTHER); ps.setBoolean(++i, errorSummary.isHasDetail()); ps.setString(++i, errorSummary.getDetailMessage()); } ps.setString(++i, job.getJobId()); }); } + + public Transfer getTransferDetails(String jobId) { + + String sql = "SELECT transfer_details FROM job WHERE job_id = ?"; + + String json = jdbcTemplate.queryForObject(sql, String.class, new Object[]{jobId}); + if (json == null) { + return null; + } + + try { + return MAPPER.readValue(json, Transfer.class); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } private String toJson(Object data) { + if (data == null) { + return null; + } try { return MAPPER.writeValueAsString(data); } catch (JsonProcessingException ex) { @@ -303,24 +339,28 @@ public class JobDAO { } public static XMLGregorianCalendar toXMLGregorianCalendar(Timestamp t) { - XMLGregorianCalendar cal = null; - try { - cal = DatatypeFactory.newInstance().newXMLGregorianCalendar(); - - LocalDateTime ldt = t.toLocalDateTime(); - - cal.setYear(ldt.getYear()); - cal.setMonth(ldt.getMonthValue()); - cal.setDay(ldt.getDayOfMonth()); - cal.setHour(ldt.getHour()); - cal.setMinute(ldt.getMinute()); - cal.setSecond(ldt.getSecond()); - cal.setFractionalSecond(new BigDecimal("0." + ldt.getNano())); - - } catch (Exception e) { - LOG.error("Error while generating XMLGregorianCalendar", e); + if (t != null) { + try { + XMLGregorianCalendar cal = DatatypeFactory.newInstance().newXMLGregorianCalendar(); + + LocalDateTime ldt = t.toLocalDateTime(); + + cal.setYear(ldt.getYear()); + cal.setMonth(ldt.getMonthValue()); + cal.setDay(ldt.getDayOfMonth()); + cal.setHour(ldt.getHour()); + cal.setMinute(ldt.getMinute()); + cal.setSecond(ldt.getSecond()); + cal.setFractionalSecond(new BigDecimal("0." + ldt.getNano())); + + // return calendar only if it has been fully initialized (otherwise + // toString issue could appear); return null in other cases. + return cal; + } catch (Exception e) { + LOG.error("Error while generating XMLGregorianCalendar", e); + } } - return cal; + return null; } } diff --git a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java index 802e5c8c8daf3bf5a4f676b4bebf06921a1a200c..b245083a981f3bd8b6ee9dffd05e587af22957a0 100644 --- a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +++ b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java @@ -10,7 +10,6 @@ import it.inaf.oats.vospace.URIUtils; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.exception.InternalFaultException; -import java.net.URISyntaxException; import java.sql.Array; import net.ivoa.xml.vospace.v2.Node; import java.sql.PreparedStatement; @@ -82,10 +81,10 @@ public class NodeDAO { } else { ps.setString(++i, jobId); } - ps.setString(++i, NodeProperties.getStandardNodePropertyByName(myNode, "creator")); - ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getStandardNodePropertyByName(myNode, "groupread"))); - ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getStandardNodePropertyByName(myNode, "groupwrite"))); - ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getStandardNodePropertyByName(myNode, "publicread"))); + ps.setString(++i, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.CREATOR_URI)); + ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.GROUP_READ_URI))); + ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(myNode, NodeProperties.GROUP_WRITE_URI))); + ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(myNode, NodeProperties.PUBLIC_READ_URI))); ps.setObject(++i, paths.get(0).getPath(), Types.OTHER); ps.setObject(++i, paths.get(0).getRelativePath(), Types.OTHER); ps.setObject(++i, NodeUtils.getDbNodeType(myNode), Types.OTHER); @@ -99,7 +98,7 @@ public class NodeDAO { String sql = "SELECT (CASE WHEN c.path = n.path THEN ? ELSE (? || ? || c.name) END) AS vos_path, c.node_id, c.name,\n" + "c.type, c.async_trans, c.sticky, c.job_id IS NOT NULL AS busy_state, c.creator_id, c.group_read, c.group_write,\n" - + "c.is_public, c.content_length, c.created_on, c.last_modified, c.accept_views, c.provide_views, c.quota\n" + + "c.is_public, c.content_length, c.created_on, c.last_modified, c.accept_views, c.provide_views, c.quota, c.content_md5\n" + "FROM node n\n" + "JOIN node c ON c.path ~ (n.path::varchar || ? || '*{1}')::lquery OR c.path = n.path\n" + "WHERE n.node_id = id_from_vos_path(?)\n" @@ -232,6 +231,9 @@ public class NodeDAO { addProperty(NodeProperties.QUOTA_URI, String.valueOf(rs.getString("quota")), properties); + addProperty(NodeProperties.MD5_URI, String.valueOf(rs.getString("content_md5")), + properties); + addProperty("urn:async_trans", String.valueOf(rs.getBoolean("async_trans")), properties); @@ -242,26 +244,9 @@ public class NodeDAO { } public Optional<Long> getNodeId(String nodeVosPath) { - String sql = "SELECT node_id FROM node_vos_path WHERE vos_path = ?"; - - List<Long> nodeIdList = jdbcTemplate.query(conn -> { - PreparedStatement ps = conn.prepareStatement(sql); - ps.setString(1, nodeVosPath); - return ps; - }, (row, index) -> { - return row.getLong("node_id"); - }); - - switch (nodeIdList.size()) { - case 0: - return Optional.empty(); - - case 1: - return Optional.of(nodeIdList.get(0)); - - default: - throw new InternalFaultException("More than 1 node id at path: " + nodeVosPath); - } + String sql = "SELECT id_from_vos_path(?) AS node_id"; + Long nodeId = jdbcTemplate.queryForObject(sql, Long.class, nodeVosPath); + return Optional.ofNullable(nodeId); } public Optional<ShortNodeDescriptor> getShortNodeDescriptor(String nodeVosPath, @@ -275,9 +260,8 @@ public class NodeDAO { + "n.type = 'container' AS is_container,\n" + "n.job_id IS NOT NULL AS busy_state\n" + "FROM node n \n" - + "JOIN node_vos_path p ON n.node_id = p.node_id \n" + "LEFT JOIN location loc ON loc.location_id = n.location_id\n" - + "WHERE vos_path = ?\n"; + + "WHERE n.node_id = id_from_vos_path(?)\n"; Optional<ShortNodeDescriptor> sndOpt = jdbcTemplate.query(conn -> { PreparedStatement ps = conn.prepareStatement(sql); diff --git a/src/test/java/it/inaf/oats/vospace/CopyServiceTest.java b/src/test/java/it/inaf/oats/vospace/CopyServiceTest.java index 3bd1aa8fda4be077c4bf22fe963ae2cdc67a336a..98045c08155443004610d97e827be81244055487 100644 --- a/src/test/java/it/inaf/oats/vospace/CopyServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/CopyServiceTest.java @@ -101,18 +101,6 @@ public class CopyServiceTest { @Test @Order(5) - public void testDontMoveIfStickySource() { - User user = mock(User.class); - when(user.getName()).thenReturn("user3"); - - assertThrows(PermissionDeniedException.class, () -> { - copyService.processCopyNodes(getTransfer("/test3/mstick", "/test4"), "job_pippo", user); - } - ); - } - - @Test - @Order(6) public void testPermissionDeniedOnExistingDestination() { User user = mock(User.class); when(user.getName()).thenReturn("user1"); @@ -125,7 +113,7 @@ public class CopyServiceTest { } @Test - @Order(7) + @Order(6) public void testDestinationExistsAndIsBusy() { User user = mock(User.class); when(user.getName()).thenReturn("user3"); @@ -137,7 +125,7 @@ public class CopyServiceTest { } @Test - @Order(9) + @Order(7) public void testCopyToExistingDestination() { User user = mock(User.class); when(user.getName()).thenReturn("user3"); @@ -172,7 +160,7 @@ public class CopyServiceTest { } @Test - @Order(10) + @Order(8) public void testCopyToExistingParent() { User user = mock(User.class); when(user.getName()).thenReturn("user3"); @@ -204,7 +192,7 @@ public class CopyServiceTest { } @Test - @Order(11) + @Order(9) public void testCopyDeniedToExistingDestination() { User user = mock(User.class); diff --git a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java index aded67f208d1b9fe9e48ef23c1190e6416e9a2e0..c5c10183d1c2ca8ae6c90a4b0ca6199cf65da559 100644 --- a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java +++ b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java @@ -5,15 +5,22 @@ */ package it.inaf.oats.vospace; +import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.FileServiceClient.ArchiveRequest; import it.inaf.oats.vospace.datamodel.Views; +import it.inaf.oats.vospace.exception.InvalidArgumentException; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; import java.util.Arrays; import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.Param; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.View; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +48,8 @@ import org.springframework.web.client.RestTemplate; @MockitoSettings(strictness = Strictness.LENIENT) public class FileServiceClientTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + @Mock private RestTemplate restTemplate; @@ -75,24 +84,97 @@ public class FileServiceClientTest { } } + @Test + public void testArchiveNoInclude() { + + Transfer transfer = new Transfer(); + transfer.setDirection("pullFromVoSpace"); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/mydir")); + View view = new View(); + view.setUri(Views.ZIP_VIEW_URI); + transfer.setView(view); + + ArchiveRequest archiveRequest = testStartArchiveJob(transfer); + + assertEquals(1, archiveRequest.getPaths().size()); + assertEquals("/mydir", archiveRequest.getPaths().get(0)); + } + + @Test + public void testInvalidViewParam() { + + Transfer transfer = new Transfer(); + transfer.setDirection("pullFromVoSpace"); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/parent_dir")); + View view = new View(); + view.setUri(Views.TAR_VIEW_URI); + transfer.setView(view); + + Param param1 = new Param(); + param1.setUri("invalid"); + param1.setValue("file1"); + view.getParam().add(param1); + + assertThrows(InvalidArgumentException.class, () -> testStartArchiveJob(transfer)); + } + + @Test + public void testInvalidViewParamPath() { + + Transfer transfer = new Transfer(); + transfer.setDirection("pullFromVoSpace"); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/parent_dir")); + View view = new View(); + view.setUri(Views.TAR_VIEW_URI); + transfer.setView(view); + + Param param1 = new Param(); + param1.setUri(Views.TAR_VIEW_URI + "/include"); + param1.setValue("../file1"); + view.getParam().add(param1); + + assertThrows(InvalidArgumentException.class, () -> testStartArchiveJob(transfer)); + } + private void testStartArchiveJob(String viewUri) { Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); - transfer.setTarget(Arrays.asList("vos://example.com!vospace/file1", "vos://example.com!vospace/file2")); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/parent_dir")); View view = new View(); view.setUri(viewUri); transfer.setView(view); + Param param1 = new Param(); + param1.setUri(viewUri + "/include"); + param1.setValue("file1"); + view.getParam().add(param1); + + Param param2 = new Param(); + param2.setUri(viewUri + "/include"); + param2.setValue("file2"); + view.getParam().add(param2); + + ArchiveRequest archiveRequest = testStartArchiveJob(transfer); + + assertEquals(2, archiveRequest.getPaths().size()); + assertEquals("/parent_dir/file1", archiveRequest.getPaths().get(0)); + assertEquals("/parent_dir/file2", archiveRequest.getPaths().get(1)); + } + + private ArchiveRequest testStartArchiveJob(Transfer transfer) { + User user = mock(User.class); when(user.getAccessToken()).thenReturn("<token>"); when(request.getUserPrincipal()).thenReturn(user); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doAnswer(invocation -> { RequestCallback requestCallback = invocation.getArgument(2); ClientHttpRequest mockedRequest = mock(ClientHttpRequest.class); HttpHeaders mockedRequestHeaders = mock(HttpHeaders.class); - when(mockedRequest.getBody()).thenReturn(new ByteArrayOutputStream()); + when(mockedRequest.getBody()).thenReturn(baos); when(mockedRequest.getHeaders()).thenReturn(mockedRequestHeaders); requestCallback.doWithRequest(mockedRequest); @@ -109,5 +191,11 @@ public class FileServiceClientTest { String redirect = fileServiceClient.startArchiveJob(transfer, "job123"); assertEquals("http://file-service/archive/result", redirect); + + try { + return MAPPER.readValue(baos.toByteArray(), ArchiveRequest.class); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } } } diff --git a/src/test/java/it/inaf/oats/vospace/JobServiceTest.java b/src/test/java/it/inaf/oats/vospace/JobServiceTest.java index 1d19b72cdb38c84383debc716371f6acf2319124..f276397fc5a30473e0a04465863632d7e7c9d6ff 100644 --- a/src/test/java/it/inaf/oats/vospace/JobServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/JobServiceTest.java @@ -8,6 +8,7 @@ package it.inaf.oats.vospace; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.persistence.JobDAO; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; @@ -15,10 +16,14 @@ import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Mockito.doAnswer; @@ -46,21 +51,27 @@ public class JobServiceTest { @Mock private HttpServletRequest servletRequest; - + @Mock private MoveService moveService; @InjectMocks private JobService jobService; + @BeforeEach + public void setUp() { + when(servletRequest.getRequestURL()).thenReturn(new StringBuffer("http://localhost/vospace/transfer")); + when(servletRequest.getContextPath()).thenReturn("/vospace"); + } + @Test public void testStartJobDefault() { - when(uriService.getTransfer(any())).thenReturn(getHttpTransfer()); + when(uriService.getTransfer(any())).thenReturn(getPullFromVoSpaceHttpTransfer()); JobSummary job = new JobSummary(); jobService.setJobPhase(job, "RUN"); - verify(jobDAO, times(2)).updateJob(job); + verify(jobDAO, times(2)).updateJob(eq(job), any()); } @Test @@ -70,7 +81,7 @@ public class JobServiceTest { JobSummary job = new JobSummary(); jobService.setJobPhase(job, "RUN"); - verify(jobDAO, times(2)).updateJob(job); + verify(jobDAO, times(2)).updateJob(eq(job), any()); } @Test @@ -82,7 +93,7 @@ public class JobServiceTest { JobSummary job = new JobSummary(); jobService.setJobPhase(job, "RUN"); - verify(jobDAO, times(2)).updateJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase()))); + verify(jobDAO, times(2)).updateJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase())), any()); } @Test @@ -94,23 +105,47 @@ public class JobServiceTest { JobSummary job = new JobSummary(); jobService.setJobPhase(job, "RUN"); - verify(jobDAO, times(2)).updateJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase()))); + verify(jobDAO, times(2)).updateJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase())), any()); } @Test public void testSyncJobResultVoSpaceError() { - when(uriService.getTransfer(any())).thenReturn(getHttpTransfer()); - doThrow(new NodeBusyException("/foo")).when(uriService).setSyncTransferEndpoints(any()); + Transfer transfer = getPullFromVoSpaceHttpTransfer(); + assertFalse(transfer.getProtocols().isEmpty()); + when(uriService.getTransfer(any())).thenReturn(transfer); + doThrow(new NodeBusyException("/foo")).when(uriService).getNegotiatedTransfer(any(), any()); jobService.createSyncJobResult(new JobSummary()); - verify(jobDAO, times(1)).createJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase()))); + verify(jobDAO, times(1)).createJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase())), any()); + assertTrue(transfer.getProtocols().isEmpty()); } @Test public void testSyncJobResultUnexpectedError() { - when(uriService.getTransfer(any())).thenReturn(getHttpTransfer()); - doThrow(new NullPointerException()).when(uriService).setSyncTransferEndpoints(any()); + Transfer transfer = getPullFromVoSpaceHttpTransfer(); + assertFalse(transfer.getProtocols().isEmpty()); + when(uriService.getTransfer(any())).thenReturn(transfer); + doThrow(new NullPointerException()).when(uriService).getNegotiatedTransfer(any(), any()); jobService.createSyncJobResult(new JobSummary()); - verify(jobDAO, times(1)).createJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase()))); + verify(jobDAO, times(1)).createJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase())), any()); + assertTrue(transfer.getProtocols().isEmpty()); + } + + @Test + public void testSyncJobResultErrorAfterNegotiatedTransfer() { + Transfer transfer = getPullFromVoSpaceHttpTransfer(); + assertFalse(transfer.getProtocols().isEmpty()); + when(uriService.getTransfer(any())).thenReturn(transfer); + + Transfer negotiatedTransfer = getPullFromVoSpaceHttpTransfer(); + assertFalse(negotiatedTransfer.getProtocols().isEmpty()); + when(uriService.getNegotiatedTransfer(any(), any())).thenReturn(negotiatedTransfer); + + doThrow(new NullPointerException()).when(servletRequest).getContextPath(); + jobService.createSyncJobResult(new JobSummary()); + + verify(jobDAO, times(1)).createJob(argThat(j -> ExecutionPhase.ERROR.equals(j.getPhase())), any()); + assertTrue(transfer.getProtocols().isEmpty()); + assertTrue(negotiatedTransfer.getProtocols().isEmpty()); } @Test @@ -134,7 +169,7 @@ public class JobServiceTest { @Test public void testStartJobSetExecutingPhaseForAsyncPullFromVoSpace() { - Transfer httpTransfer = getHttpTransfer(); + Transfer httpTransfer = getPullFromVoSpaceHttpTransfer(); JobSummary job = new JobSummary(); setJobInfo(job, httpTransfer); @@ -149,7 +184,7 @@ public class JobServiceTest { @Test public void testStartJobMoveNode() { - + Transfer moveNode = new Transfer(); moveNode.setDirection("vos://example.com!vospace/myfile"); @@ -163,20 +198,27 @@ public class JobServiceTest { JobSummary j = invocation.getArgument(0); phases.add(j.getPhase()); return null; - }).when(jobDAO).updateJob(any()); + }).when(jobDAO).updateJob(any(), any()); jobService.setJobPhase(job, "RUN"); verify(moveService, timeout(1000).times(1)).processMoveJob(any(), any()); - verify(jobDAO, times(3)).updateJob(any()); + verify(jobDAO, timeout(1000).times(3)).updateJob(any(), any()); + + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + } + assertEquals(ExecutionPhase.EXECUTING, phases.get(0)); assertEquals(ExecutionPhase.EXECUTING, phases.get(1)); assertEquals(ExecutionPhase.COMPLETED, phases.get(2)); } - private Transfer getHttpTransfer() { + private Transfer getPullFromVoSpaceHttpTransfer() { Transfer transfer = new Transfer(); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/myfile")); transfer.setDirection("pullFromVoSpace"); Protocol protocol = new Protocol(); protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); diff --git a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java index 87754f08f67bd70e59c57f9043ce86a710ee7b2e..2b6fa1f48d3cdeb6f745d59b20595c8ede9a6a13 100644 --- a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java +++ b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java @@ -8,6 +8,7 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import static it.inaf.oats.vospace.VOSpaceXmlTestUtil.loadDocument; import it.inaf.oats.vospace.datamodel.NodeProperties; +import it.inaf.oats.vospace.datamodel.Views; import it.inaf.oats.vospace.exception.ErrorSummaryFactory; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.persistence.JobDAO; @@ -20,6 +21,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; import net.ivoa.xml.uws.v1.ExecutionPhase; @@ -55,14 +57,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import org.w3c.dom.Document; import java.util.List; import net.ivoa.xml.uws.v1.ErrorSummary; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; @SpringBootTest @AutoConfigureMockMvc @ContextConfiguration(classes = {TokenFilterConfig.class}) -@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true") +@TestPropertySource(properties = {"spring.main.allow-bean-definition-overriding=true", "file-service-url=http://file-service"}) public class TransferControllerTest { @MockBean @@ -100,21 +105,10 @@ public class TransferControllerTest { @Test public void testPullFromVoSpaceAsync() throws Exception { - - Node node = mockPublicDataNode(); - when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node)); - - String requestBody = getResourceFileContent("pullFromVoSpace.xml"); - - String redirect = mockMvc.perform(post("/transfers?PHASE=RUN") - .content(requestBody) - .contentType(MediaType.APPLICATION_XML) - .accept(MediaType.APPLICATION_XML)) - .andDo(print()) - .andExpect(status().is3xxRedirection()) - .andReturn().getResponse().getHeader("Location"); - - assertThat(redirect, matchesPattern("^/transfers/.*")); + // job completion will be set by file service + String endpoint = testAsyncTransferNegotiation("/mynode", + getResourceFileContent("pullFromVoSpace.xml"), ExecutionPhase.EXECUTING); + assertTrue(endpoint.startsWith("http://file-service/mynode?jobId=")); } @Test @@ -134,15 +128,22 @@ public class TransferControllerTest { .andReturn().getResponse().getHeader("Location"); assertThat(redirect, matchesPattern("^/transfers/.*/results/transferDetails")); + + verify(jobDao, times(1)).createJob(argThat(j -> { + return ExecutionPhase.COMPLETED == j.getPhase() + && j.getResults().get(0).getHref().contains("/transferDetails"); + }), argThat(t -> { + return t.getProtocols().get(0).getEndpoint().startsWith("http://file-service/mynode?jobId="); + })); } @Test public void testPullToVoSpaceTape() throws Exception { - testPullToVoSpace("/mynode", getResourceFileContent("pullToVoSpace-tape.xml")); + testVoSpaceAsyncTransfer("/mynode", getResourceFileContent("pullToVoSpace-tape.xml")); verify(asyncTransfService, times(1)).startJob(any()); - - verify(jobDao, times(2)).updateJob(argThat(j -> ExecutionPhase.QUEUED == j.getPhase())); + + verify(jobDao, times(2)).updateJob(argThat(j -> ExecutionPhase.QUEUED == j.getPhase()), any()); } @Test @@ -150,29 +151,52 @@ public class TransferControllerTest { when(nodeDao.getNodeOsName(eq("/portalnode"))).thenReturn("file.fits"); - testPullToVoSpace("/portalnode", getResourceFileContent("pullToVoSpace-portal.xml")); + String endpoint = testAsyncTransferNegotiation("/portalnode", + getResourceFileContent("pullToVoSpace-portal.xml"), ExecutionPhase.COMPLETED); - verify(nodeDao, times(1)).setNodeLocation(eq("/portalnode"), eq(2), eq("lbcr.20130512.060722.fits.gz")); + assertTrue(endpoint.startsWith("http://archive.lbto.org")); - verify(jobDao, times(2)).updateJob(argThat(j -> { - assertTrue(j.getResults().get(0).getHref().startsWith("http://archive.lbto.org")); - assertEquals(ExecutionPhase.COMPLETED, j.getPhase()); - return true; - })); + verify(nodeDao, times(1)).setNodeLocation(eq("/portalnode"), eq(2), eq("lbcr.20130512.060722.fits.gz")); } @Test public void testPushToVoSpace() throws Exception { + // job completion will be set by file service + String endpoint = testAsyncTransferNegotiation("/uploadedfile", + getResourceFileContent("pushToVoSpace.xml"), ExecutionPhase.EXECUTING); + assertTrue(endpoint.startsWith("http://file-service/uploadedfile?jobId=")); + } - when(nodeDao.getNodeOsName(eq("/uploadedfile"))).thenReturn("file.fits"); + private String testAsyncTransferNegotiation(String path, String requestBody, ExecutionPhase endPhase) throws Exception { - testPullToVoSpace("/uploadedfile", getResourceFileContent("pushToVoSpace.xml")); - - // job completion will be set by file service - verify(jobDao, times(2)).updateJob(argThat(j -> ExecutionPhase.EXECUTING == j.getPhase())); + // detect phase updates + List<ExecutionPhase> phases = new ArrayList<>(); + List<Transfer> negotiatedTransfers = new ArrayList<>(); + doAnswer(invocation -> { + phases.add(((JobSummary) invocation.getArgument(0)).getPhase()); + negotiatedTransfers.add(invocation.getArgument(1)); + return null; + }).when(jobDao).updateJob(any(), any()); + + testVoSpaceAsyncTransfer(path, requestBody); + + ArgumentCaptor<JobSummary> jobCaptor = ArgumentCaptor.forClass(JobSummary.class); + verify(jobDao, times(2)).updateJob(jobCaptor.capture(), any()); + + assertEquals(2, phases.size()); + assertEquals(ExecutionPhase.EXECUTING, phases.get(0)); + assertEquals(endPhase, phases.get(1)); + + JobSummary job = jobCaptor.getAllValues().get(1); + assertEquals(endPhase, job.getPhase()); + assertTrue(job.getResults().get(0).getHref().contains("/transferDetails")); + + assertNull(negotiatedTransfers.get(0)); + Transfer negotiatedTransfer = negotiatedTransfers.get(1); + return negotiatedTransfer.getProtocols().get(0).getEndpoint(); } - private void testPullToVoSpace(String path, String requestBody) throws Exception { + private void testVoSpaceAsyncTransfer(String path, String requestBody) throws Exception { Node node = mockPublicDataNode(); when(nodeDao.listNode(eq(path))).thenReturn(Optional.of(node)); @@ -209,7 +233,7 @@ public class TransferControllerTest { .andExpect(status().is3xxRedirection()) .andReturn().getResponse().getHeader("Location"); - verify(jobDao, times(2)).updateJob(any()); + verify(jobDao, times(2)).updateJob(any(), any()); assertThat(redirect, matchesPattern("^/transfers/.*")); } @@ -219,6 +243,8 @@ public class TransferControllerTest { JobSummary job = getFakePendingJob(); when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job)); + + when(jobDao.getTransferDetails(eq("123"))).thenReturn(new Transfer()); mockMvc.perform(get("/transfers/123/results/transferDetails") .header("Authorization", "Bearer user1_token") @@ -280,59 +306,59 @@ public class TransferControllerTest { verify(jobDao, times(1)).getJob(eq("123")); } - + @Test - public void testErrorEndpoint() throws Exception { + public void testErrorEndpoint() throws Exception { JobSummary job = new JobSummary(); job.setJobId("123"); job.setPhase(ExecutionPhase.EXECUTING); ErrorSummary e = ErrorSummaryFactory.newErrorSummary( - new PermissionDeniedException("/pippo1/pippo2") + PermissionDeniedException.forPath("/pippo1/pippo2") ); - job.setErrorSummary(e); + job.setErrorSummary(e); when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job)); - + String response = mockMvc.perform(get("/transfers/123/error") .accept(MediaType.TEXT_PLAIN_VALUE)) .andDo(print()) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - + assertEquals("Job is not in ERROR phase", response); - + job.setPhase(ExecutionPhase.ERROR); - + response = mockMvc.perform(get("/transfers/123/error") .accept(MediaType.TEXT_PLAIN_VALUE)) .andDo(print()) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - + assertEquals(e.getDetailMessage(), response); - - e.setHasDetail(false); - + + e.setHasDetail(false); + response = mockMvc.perform(get("/transfers/123/error") .accept(MediaType.TEXT_PLAIN_VALUE)) .andDo(print()) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(); - + assertEquals("No error details available", response); - + when(jobDao.getJob(eq("124"))).thenReturn(Optional.ofNullable(null)); - + mockMvc.perform(get("/transfers/124/error") .accept(MediaType.TEXT_PLAIN_VALUE)) .andDo(print()) - .andExpect(status().is4xxClientError()); + .andExpect(status().is4xxClientError()); } @Test public void testGetJobs() throws Exception { - when(jobDao.getJobs(eq("user1"), any(), any(), any(), any())) + when(jobDao.getJobs(eq("user1"), any(), any(), any(), any(), any())) .thenReturn(this.getFakeJobs()); mockMvc.perform(get("/transfers") @@ -342,12 +368,48 @@ public class TransferControllerTest { .andDo(print()) .andExpect(status().is4xxClientError()); - String xml2 = mockMvc.perform(get("/transfers") + mockMvc.perform(get("/transfers") .header("Authorization", "Bearer user1_token") .accept(MediaType.APPLICATION_XML)) .andDo(print()) - .andExpect(status().isOk()) - .andReturn().getResponse().getContentAsString(); + .andExpect(status().isOk()); + + // direction query parameter + mockMvc.perform(get("/transfers") + .param("direction", "pullFromVoSpace") + .header("Authorization", "Bearer user1_token") + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().isOk()); + + verify(jobDao, times(1)).getJobs(eq("user1"), any(), argThat(v -> { + return v.size() == 1 && v.contains(JobService.JobDirection.pullFromVoSpace); + }), any(), any(), any()); + + // PHASE query parameter + mockMvc.perform(get("/transfers") + .param("PHASE", ExecutionPhase.EXECUTING.value()) + .header("Authorization", "Bearer user1_token") + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().isOk()); + + verify(jobDao, times(1)).getJobs(eq("user1"), argThat(v -> { + return v.size() == 1 && v.contains(ExecutionPhase.EXECUTING); + }), any(), any(), any(), any()); + + // VIEW query parameters + mockMvc.perform(get("/transfers") + .param("VIEW", Views.TAR_VIEW_URI) + .param("VIEW", Views.ZIP_VIEW_URI) + .header("Authorization", "Bearer user1_token") + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().isOk()); + + verify(jobDao, times(1)).getJobs(eq("user1"), any(), any(), argThat(v -> { + return v.size() == 2 && v.contains(Views.TAR_VIEW_URI) && v.contains(Views.ZIP_VIEW_URI); + }), any(), any()); } @Test @@ -409,7 +471,7 @@ public class TransferControllerTest { } protected static String getResourceFileContent(String fileName) throws Exception { - try (InputStream in = TransferControllerTest.class.getClassLoader().getResourceAsStream(fileName)) { + try ( InputStream in = TransferControllerTest.class.getClassLoader().getResourceAsStream(fileName)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } } diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java index d1dbe7d475aaad453e0dd7fc4f281a9275bf0617..4a59ad7ad6855ce840b7da304ce92fc13589190d 100644 --- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java @@ -24,12 +24,14 @@ import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Node; +import net.ivoa.xml.vospace.v2.Param; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.View; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,7 +67,7 @@ public class UriServiceTest { @MockBean private CreateNodeService createNodeService; - + @MockBean private FileServiceClient fileServiceClient; @@ -110,9 +112,9 @@ public class UriServiceTest { when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); JobSummary job = getJob(); - uriService.setTransferJobResult(job, uriService.getTransfer(job)); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, uriService.getTransfer(job)); - assertEquals("http://file-service/mydata1?jobId=job-id", job.getResults().get(0).getHref()); + assertEquals("http://file-service/mydata1?jobId=job-id", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test @@ -145,11 +147,11 @@ public class UriServiceTest { JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); - uriService.setTransferJobResult(job, tr); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); - assertEquals("http://file-service/mydata1?jobId=job-id&token=<new-token>", job.getResults().get(0).getHref()); + assertEquals("http://file-service/mydata1?jobId=job-id&token=<new-token>", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } - + @Test public void testPrivateUrlPermissionDenied() { @@ -180,10 +182,11 @@ public class UriServiceTest { JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); - assertThrows(PermissionDeniedException.class, - ()->{ uriService.setTransferJobResult(job, tr);}); + assertThrows(PermissionDeniedException.class, () -> { + uriService.getNegotiatedTransfer(job, tr); + }); } - + @Test public void testPrivateUrlNodeBusy() { @@ -197,8 +200,8 @@ public class UriServiceTest { readgroup.setUri(NodeProperties.GROUP_READ_URI); readgroup.setValue("group1"); node.getProperties().add(readgroup); - - node.setBusy(Boolean.TRUE); + + node.setBusy(Boolean.TRUE); when(nodeDAO.listNode(eq("/mydata1"))).thenReturn(Optional.of(node)); @@ -216,8 +219,9 @@ public class UriServiceTest { JobSummary job = getJob(); Transfer tr = uriService.getTransfer(job); - assertThrows(NodeBusyException.class, - ()->{ uriService.setTransferJobResult(job, tr);}); + assertThrows(NodeBusyException.class, () -> { + uriService.getNegotiatedTransfer(job, tr); + }); } @Test @@ -265,11 +269,11 @@ public class UriServiceTest { when(createNodeService.createNode(any(), any(), eq(user))).thenReturn(dnode); - uriService.setTransferJobResult(job, tr); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); verify(createNodeService, times(1)).createNode(any(), any(), eq(user)); - assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=<new-token>", job.getResults().get(0).getHref()); + assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=<new-token>", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } @Test @@ -312,11 +316,11 @@ public class UriServiceTest { assertEquals(2, transfer.getProtocols().size()); - uriService.setSyncTransferEndpoints(job); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); // invalid protocol is removed - assertEquals(1, transfer.getProtocols().size()); - assertEquals("ivo://ivoa.net/vospace/core#httpget", transfer.getProtocols().get(0).getUri()); + assertEquals(1, negotiatedTransfer.getProtocols().size()); + assertEquals("ivo://ivoa.net/vospace/core#httpget", negotiatedTransfer.getProtocols().get(0).getUri()); } @Test @@ -352,11 +356,11 @@ public class UriServiceTest { assertEquals(2, transfer.getProtocols().size()); - uriService.setSyncTransferEndpoints(job); + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, transfer); // invalid protocol is removed - assertEquals(1, transfer.getProtocols().size()); - assertEquals("ivo://ivoa.net/vospace/core#httpput", transfer.getProtocols().get(0).getUri()); + assertEquals(1, negotiatedTransfer.getProtocols().size()); + assertEquals("ivo://ivoa.net/vospace/core#httpput", negotiatedTransfer.getProtocols().get(0).getUri()); } @Test @@ -376,7 +380,7 @@ public class UriServiceTest { job.setJobInfo(jobInfo); try { - uriService.setSyncTransferEndpoints(job); + uriService.getNegotiatedTransfer(job, transfer); fail("Expected ProtocolNotSupportedException"); } catch (ProtocolNotSupportedException ex) { } @@ -395,7 +399,7 @@ public class UriServiceTest { job.setJobInfo(jobInfo); try { - uriService.setSyncTransferEndpoints(job); + uriService.getNegotiatedTransfer(job, transfer); fail("Expected InvalidArgumentException"); } catch (InvalidArgumentException ex) { } @@ -411,13 +415,48 @@ public class UriServiceTest { testArchiveViewEndpoint(Views.ZIP_VIEW_URI); } + @Test + public void testInvalidTransferNoProtocols() { + + Transfer transfer = new Transfer(); + transfer.setDirection("pullFromVoSpace"); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/file1")); + + JobSummary job = new JobSummary(); + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + job.setJobInfo(jobInfo); + + mockPublicNode("file1"); + mockPublicNode("file2"); + + InvalidArgumentException ex = assertThrows(InvalidArgumentException.class, () -> { + uriService.getNegotiatedTransfer(job, transfer); + }); + assertTrue(ex.getMessage().contains("no protocol")); + } + private void testArchiveViewEndpoint(String viewUri) { Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); - transfer.setTarget(Arrays.asList("vos://example.com!vospace/file1", "vos://example.com!vospace/file2")); + transfer.setTarget(Arrays.asList("vos://example.com!vospace/parent_dir")); + Protocol protocol = new Protocol(); + protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); + transfer.getProtocols().add(protocol); View view = new View(); view.setUri(viewUri); + + Param param1 = new Param(); + param1.setUri(viewUri + "/include"); + param1.setValue("file1"); + view.getParam().add(param1); + + Param param2 = new Param(); + param2.setUri(viewUri + "/include"); + param2.setValue("file2"); + view.getParam().add(param2); + transfer.setView(view); JobSummary job = new JobSummary(); @@ -426,10 +465,11 @@ public class UriServiceTest { jobInfo.getAny().add(transfer); job.setJobInfo(jobInfo); - mockPublicNode("file1"); - mockPublicNode("file2"); + mockPublicNode("parent_dir"); + mockPublicNode("parent_dir/file1"); + mockPublicNode("parent_dir/file2"); - uriService.setTransferJobResult(job, transfer); + uriService.getNegotiatedTransfer(job, transfer); verify(fileServiceClient, times(1)).startArchiveJob(transfer, "archive-job-id"); } @@ -454,6 +494,9 @@ public class UriServiceTest { Transfer transfer = new Transfer(); transfer.setTarget(Arrays.asList("vos://example.com!vospace/mydata1")); transfer.setDirection(JobService.JobDirection.pullFromVoSpace.toString()); + Protocol protocol = new Protocol(); + protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); + transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); job.setJobId("job-id"); @@ -470,6 +513,9 @@ public class UriServiceTest { Transfer transfer = new Transfer(); transfer.setTarget(Arrays.asList("vos://example.com!vospace/mydata1/mydata2")); transfer.setDirection(JobService.JobDirection.pushToVoSpace.toString()); + Protocol protocol = new Protocol(); + protocol.setUri("ivo://ivoa.net/vospace/core#httpput"); + transfer.getProtocols().add(protocol); JobSummary job = new JobSummary(); job.setJobId("job-id2"); diff --git a/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfig.java b/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfig.java index b9af33a7efabceb7ef77bc3968a9a7ff3462249c..884f870e053c31d69fbed06bc1cd9f88f07462de 100644 --- a/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfig.java +++ b/src/test/java/it/inaf/oats/vospace/persistence/DataSourceConfig.java @@ -12,9 +12,12 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.nio.file.Path; import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.sql.DataSource; @@ -24,9 +27,7 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; -import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; -import org.springframework.jdbc.datasource.init.ScriptUtils; /** * Generates a DataSource that can be used for testing DAO classes. It loads an @@ -82,15 +83,59 @@ public class DataSourceConfig { assertTrue(scriptDir.exists(), "DAO tests require " + scriptDir.getAbsolutePath() + " to exists.\n" + "Please clone the repository from https://www.ict.inaf.it/gitlab/vospace/vospace-file-catalog.git"); - File[] scripts = scriptDir.listFiles(f -> f.getName().endsWith(".sql")); - Arrays.sort(scripts); // sort alphabetically + // load all sql files in vospace-file-catalog repo + File[] repoScripts = scriptDir.listFiles(f -> f.getName().endsWith(".sql")); + Arrays.sort(repoScripts); // sort alphabetically + + // add test-data.sql + List<File> scripts = new ArrayList<>(Arrays.asList(repoScripts)); + scripts.add(new ClassPathResource("test-data.sql").getFile()); for (File script : scripts) { - ByteArrayResource scriptResource = replaceDollarQuoting(script.toPath()); - ScriptUtils.executeSqlScript(conn, scriptResource); + String scriptContent = Files.readString(script.toPath()); + for (String sql : splitScript(scriptContent)) { + executeSql(conn, replaceDollarQuoting(sql)); + } } + } + } - ScriptUtils.executeSqlScript(conn, new ClassPathResource("test-data.sql")); + /** + * Spring ScriptUtils is not able to correctly split the SQL statements if a + * function definition contains semicolon characters, so this method is used + * instead of it. + */ + private List<String> splitScript(String script) { + + List<String> parts = new ArrayList<>(); + + StringBuilder sb = new StringBuilder(); + + boolean insideFunc = false; + for (int i = 0; i < script.length(); i++) { + char c = script.charAt(i); + sb.append(c); + + if (insideFunc) { + if (i > 6 && "$func$".equals(script.substring(i - 6, i))) { + insideFunc = false; + } + } else { + if (i > 6 && "$func$".equals(script.substring(i - 6, i))) { + insideFunc = true; + } else if (c == ';') { + parts.add(sb.toString()); + sb = new StringBuilder(); + } + } + } + + return parts; + } + + private void executeSql(Connection conn, String sqlStatement) throws SQLException { + try ( Statement stat = conn.createStatement()) { + stat.execute(sqlStatement); } } @@ -100,9 +145,7 @@ public class DataSourceConfig { * instead of inside the original files because dollar quoting provides a * better visibility. */ - private ByteArrayResource replaceDollarQuoting(Path sqlScriptPath) throws Exception { - - String scriptContent = Files.readString(sqlScriptPath); + private String replaceDollarQuoting(String scriptContent) { if (scriptContent.contains("$func$")) { @@ -114,7 +157,7 @@ public class DataSourceConfig { scriptContent = scriptContent.replace(originalFunction, newFunction); } - return new ByteArrayResource(scriptContent.getBytes()); + return scriptContent; } private String extractFunctionDefinition(String scriptContent) { diff --git a/src/test/java/it/inaf/oats/vospace/persistence/JobDAOTest.java b/src/test/java/it/inaf/oats/vospace/persistence/JobDAOTest.java index a18f8d4a891fd836882b548d0b71dc4e242477e1..69b0f8e0a1a2206c14ba85e67b8d47765b616e8a 100644 --- a/src/test/java/it/inaf/oats/vospace/persistence/JobDAOTest.java +++ b/src/test/java/it/inaf/oats/vospace/persistence/JobDAOTest.java @@ -6,6 +6,7 @@ package it.inaf.oats.vospace.persistence; import it.inaf.oats.vospace.JobService; +import it.inaf.oats.vospace.datamodel.Views; import java.util.List; import javax.sql.DataSource; import net.ivoa.xml.uws.v1.ExecutionPhase; @@ -29,6 +30,9 @@ import net.ivoa.xml.uws.v1.Jobs; import it.inaf.oats.vospace.exception.ErrorSummaryFactory; import it.inaf.oats.vospace.exception.PermissionDeniedException; import java.util.Arrays; +import net.ivoa.xml.uws.v1.ResultReference; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {DataSourceConfig.class}) @@ -73,18 +77,64 @@ public class JobDAOTest { JobSummary job = getJob(); - dao.createJob(job); + dao.createJob(job, null); assertTrue(dao.getJob("123").isPresent()); assertEquals(ExecutionPhase.PENDING, dao.getJob("123").get().getPhase()); // uses the job retrieved from DAO to perform the update (reproduced a bug in job update) job = dao.getJob("123").get(); + assertNotNull(job.getCreationTime()); + assertNull(job.getStartTime()); job.setPhase(ExecutionPhase.EXECUTING); - dao.updateJob(job); + dao.updateJob(job, null); - assertEquals(ExecutionPhase.EXECUTING, dao.getJob("123").get().getPhase()); + job = dao.getJob("123").get(); + assertEquals(ExecutionPhase.EXECUTING, job.getPhase()); + assertNotNull(job.getStartTime()); + assertNull(job.getEndTime()); + + assertNull(dao.getTransferDetails(job.getJobId())); + + Transfer negotiatedTransfer = new Transfer(); + job.setPhase(ExecutionPhase.COMPLETED); + dao.updateJob(job, negotiatedTransfer); + + job = dao.getJob("123").get(); + assertEquals(ExecutionPhase.COMPLETED, job.getPhase()); + assertNotNull(job.getStartTime()); + assertNotNull(job.getEndTime()); + assertNotNull(dao.getTransferDetails(job.getJobId())); + } + + /** + * Jobs created by the /synctrans endpoint contains results list at creation time. + */ + @Test + public void testCreateJobWithResults() { + JobSummary job = getJob(); + job.setPhase(ExecutionPhase.COMPLETED); + + ResultReference result = new ResultReference(); + result.setId("transferDetails"); + result.setHref("http://ia2.inaf.it"); + job.getResults().add(result); + + Transfer negotiatedTransfer = new Transfer(); + + dao.createJob(job, negotiatedTransfer); + + // Retrieve it back + Optional<JobSummary> retrievedJobOpt = dao.getJob(job.getJobId()); + assertTrue(retrievedJobOpt.isPresent()); + + JobSummary retrievedJob = retrievedJobOpt.get(); + assertEquals(1, retrievedJob.getResults().size()); + assertNotNull(retrievedJob.getStartTime()); + assertNotNull(retrievedJob.getEndTime()); + + assertNotNull(dao.getTransferDetails(retrievedJob.getJobId())); } @Test @@ -96,7 +146,7 @@ public class JobDAOTest { // Generate it from exception ErrorSummary errorSummary = ErrorSummaryFactory.newErrorSummary( - new PermissionDeniedException("/pippo1/pippo2")); + PermissionDeniedException.forPath("/pippo1/pippo2")); // Check if properly generated assertTrue(errorSummary.isHasDetail()); @@ -104,7 +154,7 @@ public class JobDAOTest { job.setErrorSummary(errorSummary); - dao.createJob(job); + dao.createJob(job, null); // Retrieve it back Optional<JobSummary> retrievedJobOpt = dao.getJob(job.getJobId()); @@ -113,20 +163,21 @@ public class JobDAOTest { JobSummary retrievedJob = retrievedJobOpt.get(); assertEquals(ExecutionPhase.ERROR, retrievedJob.getPhase()); assertTrue(areEqual(job.getErrorSummary(), retrievedJob.getErrorSummary())); - + assertNotNull(retrievedJob.getStartTime()); + assertNotNull(retrievedJob.getEndTime()); } @Test public void testUpdateJobWithError() { JobSummary job = getJob(); - dao.createJob(job); + dao.createJob(job, null); job.setPhase(ExecutionPhase.ERROR); // Generate it from exception ErrorSummary errorSummary = ErrorSummaryFactory.newErrorSummary( - new PermissionDeniedException("/pippo1/pippo2")); + PermissionDeniedException.forPath("/pippo1/pippo2")); // Check if properly generated assertTrue(errorSummary.isHasDetail()); @@ -134,7 +185,7 @@ public class JobDAOTest { job.setErrorSummary(errorSummary); - dao.updateJob(job); + dao.updateJob(job, null); // Retrieve it back Optional<JobSummary> retrievedJobOpt = dao.getJob(job.getJobId()); @@ -143,6 +194,7 @@ public class JobDAOTest { JobSummary retrievedJob = retrievedJobOpt.get(); assertEquals(ExecutionPhase.ERROR, retrievedJob.getPhase()); assertTrue(areEqual(job.getErrorSummary(), retrievedJob.getErrorSummary())); + assertNotNull(retrievedJob.getEndTime()); } @Test @@ -151,10 +203,11 @@ public class JobDAOTest { String user = "user1"; List<ExecutionPhase> phaseList = List.of(); List<JobService.JobDirection> directionList = List.of(); + List<String> viewList = List.of(); Optional<LocalDateTime> after = Optional.ofNullable(null); Optional<Integer> last = Optional.ofNullable(null); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); assertTrue(jobs != null); List<ShortJobDescription> sjdList = jobs.getJobref(); @@ -176,10 +229,11 @@ public class JobDAOTest { String user = "user1"; List<ExecutionPhase> phaseList = List.of(); List<JobService.JobDirection> directionList = List.of(); + List<String> viewList = List.of(); Optional<LocalDateTime> after = Optional.ofNullable(null); Optional<Integer> last = Optional.of(2); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); List<ShortJobDescription> sjdList = jobs.getJobref(); assertEquals(2, sjdList.size()); @@ -192,10 +246,11 @@ public class JobDAOTest { List<ExecutionPhase> phaseList = List.of(ExecutionPhase.PENDING, ExecutionPhase.EXECUTING); List<JobService.JobDirection> directionList = List.of(); + List<String> viewList = List.of(); Optional<LocalDateTime> after = Optional.ofNullable(null); Optional<Integer> last = Optional.ofNullable(null); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); List<ShortJobDescription> sjdList = jobs.getJobref(); assertEquals(sjdList.size(), 2); assertEquals("pippo5", sjdList.get(0).getId()); @@ -210,10 +265,11 @@ public class JobDAOTest { List<JobService.JobDirection> directionList = List.of(JobService.JobDirection.pullFromVoSpace, JobService.JobDirection.pullToVoSpace); + List<String> viewList = List.of(); Optional<LocalDateTime> after = Optional.ofNullable(null); Optional<Integer> last = Optional.ofNullable(null); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); List<ShortJobDescription> sjdList = jobs.getJobref(); assertEquals(2, sjdList.size()); assertEquals("pippo3", sjdList.get(0).getId()); @@ -226,13 +282,14 @@ public class JobDAOTest { String user = "user1"; List<ExecutionPhase> phaseList = List.of(); List<JobService.JobDirection> directionList = List.of(); + List<String> viewList = List.of(); LocalDateTime ldt = LocalDateTime.of(2013, Month.FEBRUARY, 7, 18, 15); Optional<LocalDateTime> after = Optional.of(ldt); Optional<Integer> last = Optional.ofNullable(null); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); List<ShortJobDescription> sjdList = jobs.getJobref(); assertEquals(2, sjdList.size()); assertEquals("pippo5", sjdList.get(0).getId()); @@ -248,13 +305,14 @@ public class JobDAOTest { List<JobService.JobDirection> directionList = List.of(JobService.JobDirection.pullFromVoSpace, JobService.JobDirection.pullToVoSpace); + List<String> viewList = List.of(Views.TAR_VIEW_URI, Views.ZIP_VIEW_URI); LocalDateTime ldt = LocalDateTime.of(2013, Month.FEBRUARY, 7, 18, 15); Optional<LocalDateTime> after = Optional.of(ldt); Optional<Integer> last = Optional.of(2); - Jobs jobs = dao.getJobs(user, phaseList, directionList, after, last); + Jobs jobs = dao.getJobs(user, phaseList, directionList, viewList, after, last); List<ShortJobDescription> sjdList = jobs.getJobref(); assertEquals(1, sjdList.size()); assertEquals("pippo3", sjdList.get(0).getId()); diff --git a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java index dd03781ce8d5f39dd06b5047d9830d598cafa278..98e4e10644c57b7b897d4ab14bf0526b793a3d0a 100644 --- a/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java +++ b/src/test/java/it/inaf/oats/vospace/persistence/NodeDAOTest.java @@ -89,6 +89,17 @@ public class NodeDAOTest { assertFalse(children.isEmpty()); assertTrue(children.size() == 2); assertTrue(children.containsAll(List.of("f4", "f5"))); + + } + + @Test + public void testGetQuotaAndMD5() { + + ContainerNode node = (ContainerNode) dao.listNode("/test1/f1/f2_renamed").get(); + assertEquals("50000", NodeProperties.getNodePropertyByURI(node, NodeProperties.QUOTA_URI)); + DataNode child = (DataNode) node.getNodes().get(0); + assertEquals("4000", NodeProperties.getNodePropertyByURI(child, NodeProperties.LENGTH_URI)); + assertEquals("<md5sum>", NodeProperties.getNodePropertyByURI(child, NodeProperties.MD5_URI)); } @Test diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql index 4b0af3bd45cb04a2b2f1d4739741f3bae4cec594..44a025cd80f440faad28393851b4e25892da41f7 100644 --- a/src/test/resources/test-data.sql +++ b/src/test/resources/test-data.sql @@ -16,8 +16,8 @@ INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, loc INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_read, group_write, location_id) VALUES ('', NULL, 'test1', 'container', 'user1', '{"group1","group2"}','{"group2"}', 1); -- /test1 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, location_id) VALUES ('2', '', 'f1', 'container', 'user1', 1); -- /test1/f1 (rel: /f1) -INSERT INTO node (parent_path, parent_relative_path, name, os_name, type, creator_id, location_id) VALUES ('2.3', '3', 'f2_renamed', 'f2', 'container', 'user1', 1); -- /test1/f1/f2_renamed (rel: /f1/f2) -INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, location_id) VALUES ('2.3.4', '3.4', 'f3', 'data', 'user1', 1); -- /test1/f1/f2_renamed/f3 (rel: /f1/f2/f3) +INSERT INTO node (parent_path, parent_relative_path, name, os_name, type, creator_id, location_id, quota) VALUES ('2.3', '3', 'f2_renamed', 'f2', 'container', 'user1', 1, 50000); -- /test1/f1/f2_renamed (rel: /f1/f2) +INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, location_id, content_md5, content_length) VALUES ('2.3.4', '3.4', 'f3', 'data', 'user1', 1, '<md5sum>', 4000); -- /test1/f1/f2_renamed/f3 (rel: /f1/f2/f3) INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('', NULL, 'test2', 'container', 'user2', true, 1); -- /test2 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, is_public, location_id) VALUES ('6', '', 'f4', 'container', 'user2', true, 1); -- /test2/f4 (rel: /f4) @@ -40,7 +40,7 @@ DELETE FROM job; INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo1', 'user1', 'pullFromVoSpace', 'ARCHIVED', NULL, NULL, '2011-06-22 19:10:25', NULL, NULL); INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo2', 'user1', 'pullToVoSpace', 'PENDING', NULL, NULL, '2012-06-22 19:10:25', NULL, NULL); -INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo3', 'user1', 'pullFromVoSpace', 'QUEUED', NULL, NULL, '2013-06-22 19:10:25', NULL, NULL); +INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo3', 'user1', 'pullFromVoSpace', 'QUEUED', NULL, NULL, '2013-06-22 19:10:25', '{"transfer": {"view": {"uri": "ivo://ia2.inaf.it/vospace/views#zip"}}}', NULL); INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo4', 'user2', 'copyNode', 'PENDING', NULL, NULL, '2014-06-22 19:10:25', NULL, NULL); INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo5', 'user1', 'pushToVoSpace', 'EXECUTING', NULL, NULL, '2015-06-22 19:10:25', NULL, NULL); INSERT INTO job (job_id, owner_id, job_type, phase, start_time, end_time, creation_time, job_info, results) VALUES ('pippo6', 'user2', 'pullFromVoSpace', 'PENDING', NULL, NULL, '2015-06-22 19:10:25', NULL, NULL);