package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.persistence.JobDAO; import java.util.Optional; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.uws.v1.Jobs; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; import java.util.Arrays; import java.util.HashSet; import java.util.List; import net.ivoa.xml.uws.v1.ErrorSummary; import net.ivoa.xml.vospace.v2.Protocol; @RestController public class TransferController { @Autowired private JobDAO jobDAO; @Autowired private HttpServletRequest request; @Autowired private JobService jobService; @PostMapping(value = "/transfers", consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity postAsyncTransfer(@RequestBody Transfer transfer, @RequestParam(value = "PHASE", required = false) Optional phase, User principal) { JobSummary jobSummary = newJobSummary(transfer, principal); jobDAO.createJob(jobSummary); if (phase.isPresent()) { jobService.setJobPhase(jobSummary, phase.get()); } return getJobRedirect(jobSummary.getJobId()); } @PostMapping(value = "/synctrans", consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity postSyncTransfer(@RequestBody Transfer transfer, User principal) { JobSummary jobSummary = newJobSummary(transfer, principal); jobService.createSyncJobResult(jobSummary); HttpHeaders headers = new HttpHeaders(); headers.set("Location", request.getContextPath() + "/transfers/" + jobSummary.getJobId() + "/results/transferDetails"); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); } @GetMapping("/synctrans") public ResponseEntity syncTransferUrlParamsMode(@RequestParam("TARGET") String target, @RequestParam("DIRECTION") String direction, @RequestParam("PROTOCOL") String protocolUris, User principal) { Transfer transfer = new Transfer(); transfer.setTarget(target); transfer.setDirection(direction); // CADC client sends multiple protocol parameters and Spring join them using a comma separator. // Some values seems to be duplicated, so a set is used to avoid the duplication for (String protocolUri : new HashSet<>(Arrays.asList(protocolUris.split(",")))) { Protocol protocol = new Protocol(); protocol.setUri(protocolUri); transfer.getProtocols().add(protocol); } JobSummary jobSummary = newJobSummary(transfer, principal); jobService.createSyncJobResult(jobSummary); if (jobSummary.getErrorSummary() != null) { // TODO: decide how to hanlde HTTP error codes // If an error occurs with the synchronous convenience mode where the preferred endpoint // is immediately returned as a redirect, the error information is returned directly in // the response body with the associated HTTP status code. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(jobSummary.getErrorSummary().getMessage()); } // Behaves as if REQUEST=redirect was set, for compatibility with CADC client String endpoint = transfer.getProtocols().get(0).getEndpoint(); HttpHeaders headers = new HttpHeaders(); headers.set("Location", endpoint); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); } @GetMapping(value = "/transfers/{jobId}", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity getJob(@PathVariable("jobId") String jobId) { return jobDAO.getJob(jobId).map(j -> ResponseEntity.ok(j)).orElse(ResponseEntity.notFound().build()); } @GetMapping(value = "/transfers/{jobId}/error", produces = {MediaType.TEXT_PLAIN_VALUE}) public ResponseEntity getJobError(@PathVariable("jobId") String jobId) { return jobDAO.getJob(jobId).map(j -> { if (j.getPhase().equals(ExecutionPhase.ERROR)) { ErrorSummary e = j.getErrorSummary(); if (e.isHasDetail()) { return ResponseEntity.ok(e.getDetailMessage()); } else { return ResponseEntity.ok("No error details available"); } } else { return ResponseEntity.ok("Job is not in ERROR phase"); } }).orElse(ResponseEntity.notFound().build()); } @PostMapping(value = "/transfers/{jobId}/phase") public ResponseEntity setJobPhase(@PathVariable("jobId") String jobId, @RequestParam("PHASE") String phase, User principal) { return jobDAO.getJob(jobId).map(job -> { if (!job.getOwnerId().equals(principal.getName())) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } jobService.setJobPhase(job, phase); return getJobRedirect(job.getJobId()); }).orElse(ResponseEntity.notFound().build()); } @GetMapping(value = "/transfers/{jobId}/results/transferDetails", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity getTransferDetails(@PathVariable("jobId") String jobId, User principal) { return jobDAO.getJob(jobId).map(job -> { if (!job.getOwnerId().equals(principal.getName())) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // TODO: check type return ResponseEntity.ok((Transfer) (job.getJobInfo().getAny().get(0))); }).orElse(ResponseEntity.notFound().build()); } @GetMapping(value = "/transfers/{jobId}/phase", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity getTransferJobPhase(@PathVariable("jobId") String jobId, User principal) { // TODO: error handling return ResponseEntity.ok(jobDAO.getJob(jobId).get().getPhase().toString()); } @GetMapping(value = "/transfers", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity getTransfers( @RequestParam(value = "PHASE", required = false) Optional> phase, @RequestParam(value = "AFTER", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Optional after, @RequestParam(value = "LAST", required = false) Optional last, @RequestParam(value = "direction", required = false) Optional> direction, User principal) { if (last.isPresent()) { if (last.get() <= 0) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } } String userId = principal.getName(); List phaseList; if (phase.isPresent()) { phaseList = phase.get(); } else { phaseList = List.of(); } List directionList; if (direction.isPresent()) { directionList = direction.get(); } else { directionList = List.of(); } Jobs jobs = jobDAO.getJobs(userId, phaseList, directionList, after, last); return ResponseEntity.ok(jobs); } private JobSummary newJobSummary(Transfer transfer, User principal) { String jobId = UUID.randomUUID().toString().replace("-", ""); JobSummary jobSummary = new JobSummary(); jobSummary.setJobId(jobId); jobSummary.setOwnerId(principal.getName()); jobSummary.setPhase(ExecutionPhase.PENDING); JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); jobInfo.getAny().add(transfer); jobSummary.setJobInfo(jobInfo); return jobSummary; } private ResponseEntity getJobRedirect(String jobId) { HttpHeaders headers = new HttpHeaders(); headers.set("Location", request.getContextPath() + "/transfers/" + jobId); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); } }