@Test(expected = WebApplicationException.class) public void testFetchJobDetailsByIdThrowsWebApplicationExceptionIfJobIdIsNull() throws IOException { final JobDAO jobDAO = mockJobDAOThatReturns(Optional.empty()); final JobResource jobResource = resourceThatUses(jobDAO); jobResource.getJobDetailsById(TestHelpers.generateSecureSecurityContext(), null); }
@GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll public Optional<APIJobDetails> getJobDetailsById( @Context SecurityContext context, @Parameter(description = "The job's ID") @PathParam("job-id") @NotNull JobId jobId) { if (jobId == null) throw new WebApplicationException("Job ID is null", 400); return jobDAO.getJobDetailsById(jobId).map(this::toJobResponse); }
JobResource { @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll public Optional<APIJobDetails> getJobDetailsById( @Context SecurityContext context, @Parameter(description = "The job's ID") @PathParam("job-id") @NotNull JobId jobId) { if (jobId == null) throw new WebApplicationException("Job ID is null", 400); return jobDAO.getJobDetailsById(jobId).map(this::toJobResponse); } }
JobResource { @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll public Optional<APIJobDetails> getJobDetailsById( @Context SecurityContext context, @Parameter(description = "The job's ID") @PathParam("job-id") @NotNull JobId jobId) { if (jobId == null) throw new WebApplicationException("Job ID is null", 400); return jobDAO.getJobDetailsById(jobId).map(this::toJobResponse); } JobResource(
            JobManagerActions jobManagerActions,
            JobDAO jobDAO,
            JobSpecConfigurationDAO jobSpecConfigurationDAO,
            int defaultPageSize); }
JobResource { @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll public Optional<APIJobDetails> getJobDetailsById( @Context SecurityContext context, @Parameter(description = "The job's ID") @PathParam("job-id") @NotNull JobId jobId) { if (jobId == null) throw new WebApplicationException("Job ID is null", 400); return jobDAO.getJobDetailsById(jobId).map(this::toJobResponse); } JobResource(
            JobManagerActions jobManagerActions,
            JobDAO jobDAO,
            JobSpecConfigurationDAO jobSpecConfigurationDAO,
            int defaultPageSize); @GET @Operation( summary = "Retrieve jobs managed by the system.", description = "Gets *some* of the jobs managed by the system. The response does not necessarily " + "contain *all* the jobs managed by the system because pagination " + "and client permissions may hide entries. ") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Entries returned", content = @Content( schema = @Schema(implementation = APIJobDetailsCollection.class) )) }) @PermitAll APIJobDetailsCollection getJobs(
            @Context
                    SecurityContext context,
            @Parameter(description = "The page number (0-indexed)")
            @QueryParam("page")
                    Optional<Integer> page,
            @Parameter(description = "The number of entries a response page should contain. Max page size is " + MAX_PAGE_SIZE)
            @QueryParam("page-size")
                    Optional<Integer> pageSize,
            @Parameter(description = "Client query string")
            @QueryParam("query")
                    Optional<String> query); @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll Optional<APIJobDetails> getJobDetailsById(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job's ID")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @DELETE @Path("{job-id}") @Operation( summary = "Delete a job from the system", description = "Deletes a job from the system, removing **all** job data. Running jobs are aborted before deletion." ) @PermitAll int deleteJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job's ID")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @POST @Operation( summary = "Submit a new job", description = "Attempt to submit a new job to the system. The system will check the job against " + "the job spec specified in the request. If it does not match, the request will be " + "immediately rejected by the server. Otherwise, the request will be immediately accepted " + "by the server. Note: the server accepting the job is only indicates that the request " + "matches the job spec. It does not guarantee that the underlying job will complete " + "successfully.") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job request accepted", content = @Content( schema = @Schema(implementation = APIJobCreatedResponse.class) )), @ApiResponse( responseCode = "400", description = "Invalid or malformed job request", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll APIJobCreatedResponse submitJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job request")
            @NotNull
            @Valid
                    APIJobRequest apiJobRequest); static Either<ValidJobRequest, List<ValidationError>> validateAPIRequest(
            APIJobRequest APIJobRequest,
            JobSpecConfigurationDAO jobSpecConfigurationDAO,
            UserId userId); @POST @Path("/{job-id}/abort") @Operation( summary = "Abort a running job", description = "Abort a job, stopping it or removing it from the job execute. The job's status " + "should immediately change to aborting. However, full job abortion is not guaranteed " + "to be immediate. This is because the underlying job may take time to close gracefully " + "or because the system itself has a short delay before forcibly killing the job outright.") @PermitAll void abortJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to abort")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/stdout") @Operation( summary = "Get a job's standard output", description = "Get a job's standard output, if available. A job that has not yet started will not have a standard output and, " + "therefore, this method will return a 404. There is no guarantee that all running/finished jobs will have standard output " + "data. This is because administrative and cleanup routines may dequeue a job's output in order to save space on the server. ") @Produces(DEFAULT_BINARY_MIME_TYPE) @PermitAll Response fetchJobStdoutById(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get stdout for")
            @PathParam("job-id")
            @NotNull
            JobId jobId); @GET @Path("/{job-id}/stderr") @Operation( summary = "Get the job's standard error", description = "Get the job's standard error, if available. A job that has not yet started will not have a standard error and, " + "therefore, this method will return a 404. There is no guarantee that all running/finished jobs will have standard " + "error data. This is because administrative and cleanup routines may dequeue a job's output in order to save space on " + "the server.") @Produces(DEFAULT_BINARY_MIME_TYPE) @PermitAll Response fetchJobStderrById(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get stderr for")
            @PathParam("job-id")
            @NotNull
            JobId jobId); @GET @Path("/{job-id}/spec") @Operation( summary = "Get the spec the job was submitted against", description = "Get the spec the job was submitted against. Note: This returns the spec as it was when the " + "job was submitted. Any subsequent updates to the spec will not be in the spec returned by this API call.") @PermitAll Optional<APIJobSpec> fetchJobSpecJobWasSubmittedAgainst(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the spec for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/inputs") @Operation( summary = "Get the job's inputs", description = "Get the inputs that were supplied when the job was submitted.") @PermitAll Optional<Map<JobExpectedInputId, JsonNode>> fetchJobInputs(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get inputs for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/outputs") @Operation( summary = "Get the outputs produced by the job", description = "Gets all the outputs produced by the job. If the job has not *written* any outputs (even if specified) " + "then an empty map is returned. If the job does not exist, a 404 is returned") @PermitAll APIJobOutputCollection fetchJobOutputs(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the outputs for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/outputs/{output-id}") @Operation( summary = "Get an output produced by the job", description = "Gets an output produced by the job. If the job has not written this output, of it it has been " + "subsequently deleted, a 404 shall be returned") @PermitAll Response fetchJobOutput(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the output for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId,
            @Parameter(description = "ID of the output")
            @PathParam("output-id")
            @NotNull
                    JobOutputId outputId); }
JobResource { @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll public Optional<APIJobDetails> getJobDetailsById( @Context SecurityContext context, @Parameter(description = "The job's ID") @PathParam("job-id") @NotNull JobId jobId) { if (jobId == null) throw new WebApplicationException("Job ID is null", 400); return jobDAO.getJobDetailsById(jobId).map(this::toJobResponse); } JobResource(
            JobManagerActions jobManagerActions,
            JobDAO jobDAO,
            JobSpecConfigurationDAO jobSpecConfigurationDAO,
            int defaultPageSize); @GET @Operation( summary = "Retrieve jobs managed by the system.", description = "Gets *some* of the jobs managed by the system. The response does not necessarily " + "contain *all* the jobs managed by the system because pagination " + "and client permissions may hide entries. ") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Entries returned", content = @Content( schema = @Schema(implementation = APIJobDetailsCollection.class) )) }) @PermitAll APIJobDetailsCollection getJobs(
            @Context
                    SecurityContext context,
            @Parameter(description = "The page number (0-indexed)")
            @QueryParam("page")
                    Optional<Integer> page,
            @Parameter(description = "The number of entries a response page should contain. Max page size is " + MAX_PAGE_SIZE)
            @QueryParam("page-size")
                    Optional<Integer> pageSize,
            @Parameter(description = "Client query string")
            @QueryParam("query")
                    Optional<String> query); @GET @Path("{job-id}") @Operation( summary = "Get details of a job managed by the system.", description = "") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job details found", content = @Content( schema = @Schema(implementation = APIJobDetails.class) )), @ApiResponse( responseCode = "404", description = "The job could not be found", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )), @ApiResponse( responseCode = "401", description = "Client not authorized to request job details", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll Optional<APIJobDetails> getJobDetailsById(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job's ID")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @DELETE @Path("{job-id}") @Operation( summary = "Delete a job from the system", description = "Deletes a job from the system, removing **all** job data. Running jobs are aborted before deletion." ) @PermitAll int deleteJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job's ID")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @POST @Operation( summary = "Submit a new job", description = "Attempt to submit a new job to the system. The system will check the job against " + "the job spec specified in the request. If it does not match, the request will be " + "immediately rejected by the server. Otherwise, the request will be immediately accepted " + "by the server. Note: the server accepting the job is only indicates that the request " + "matches the job spec. It does not guarantee that the underlying job will complete " + "successfully.") @ApiResponses(value = { @ApiResponse( responseCode = "200", description = "Job request accepted", content = @Content( schema = @Schema(implementation = APIJobCreatedResponse.class) )), @ApiResponse( responseCode = "400", description = "Invalid or malformed job request", content = @Content( schema = @Schema(implementation = APIErrorMessage.class) )) }) @PermitAll APIJobCreatedResponse submitJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "The job request")
            @NotNull
            @Valid
                    APIJobRequest apiJobRequest); static Either<ValidJobRequest, List<ValidationError>> validateAPIRequest(
            APIJobRequest APIJobRequest,
            JobSpecConfigurationDAO jobSpecConfigurationDAO,
            UserId userId); @POST @Path("/{job-id}/abort") @Operation( summary = "Abort a running job", description = "Abort a job, stopping it or removing it from the job execute. The job's status " + "should immediately change to aborting. However, full job abortion is not guaranteed " + "to be immediate. This is because the underlying job may take time to close gracefully " + "or because the system itself has a short delay before forcibly killing the job outright.") @PermitAll void abortJob(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to abort")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/stdout") @Operation( summary = "Get a job's standard output", description = "Get a job's standard output, if available. A job that has not yet started will not have a standard output and, " + "therefore, this method will return a 404. There is no guarantee that all running/finished jobs will have standard output " + "data. This is because administrative and cleanup routines may dequeue a job's output in order to save space on the server. ") @Produces(DEFAULT_BINARY_MIME_TYPE) @PermitAll Response fetchJobStdoutById(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get stdout for")
            @PathParam("job-id")
            @NotNull
            JobId jobId); @GET @Path("/{job-id}/stderr") @Operation( summary = "Get the job's standard error", description = "Get the job's standard error, if available. A job that has not yet started will not have a standard error and, " + "therefore, this method will return a 404. There is no guarantee that all running/finished jobs will have standard " + "error data. This is because administrative and cleanup routines may dequeue a job's output in order to save space on " + "the server.") @Produces(DEFAULT_BINARY_MIME_TYPE) @PermitAll Response fetchJobStderrById(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get stderr for")
            @PathParam("job-id")
            @NotNull
            JobId jobId); @GET @Path("/{job-id}/spec") @Operation( summary = "Get the spec the job was submitted against", description = "Get the spec the job was submitted against. Note: This returns the spec as it was when the " + "job was submitted. Any subsequent updates to the spec will not be in the spec returned by this API call.") @PermitAll Optional<APIJobSpec> fetchJobSpecJobWasSubmittedAgainst(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the spec for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/inputs") @Operation( summary = "Get the job's inputs", description = "Get the inputs that were supplied when the job was submitted.") @PermitAll Optional<Map<JobExpectedInputId, JsonNode>> fetchJobInputs(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get inputs for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/outputs") @Operation( summary = "Get the outputs produced by the job", description = "Gets all the outputs produced by the job. If the job has not *written* any outputs (even if specified) " + "then an empty map is returned. If the job does not exist, a 404 is returned") @PermitAll APIJobOutputCollection fetchJobOutputs(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the outputs for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId); @GET @Path("/{job-id}/outputs/{output-id}") @Operation( summary = "Get an output produced by the job", description = "Gets an output produced by the job. If the job has not written this output, of it it has been " + "subsequently deleted, a 404 shall be returned") @PermitAll Response fetchJobOutput(
            @Context
                    SecurityContext context,
            @Parameter(description = "ID of the job to get the output for")
            @PathParam("job-id")
            @NotNull
                    JobId jobId,
            @Parameter(description = "ID of the output")
            @PathParam("output-id")
            @NotNull
                    JobOutputId outputId);  }