@Test void canHandleForwardJobOutputRequestWithSuccess() throws IOException, GenieException, GenieCheckedException { this.jobsProperties.getForwarding().setEnabled(true); final String jobId = UUID.randomUUID().toString(); final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Mockito.mock(HttpServletResponse.class); final String jobHostName = UUID.randomUUID().toString(); Mockito .when(request.getRequestURL()) .thenReturn(new StringBuffer("http: Mockito .doNothing() .when(this.jobDirectoryServerService) .serveResource( Mockito.eq(jobId), Mockito.any(URL.class), Mockito.anyString(), Mockito.eq(request), Mockito.eq(response) ); Mockito.when(this.persistenceService.getJobArchiveStatus(jobId)).thenReturn(ArchiveStatus.PENDING); Mockito .when(this.agentRoutingService.getHostnameForAgentConnection(jobId)) .thenReturn(Optional.of(jobHostName)); final String http = "http"; Mockito.when(request.getScheme()).thenReturn(http); final int port = 8080; Mockito.when(request.getServerPort()).thenReturn(port); final String requestURI = "/" + jobId + "/" + UUID.randomUUID().toString(); Mockito.when(request.getRequestURI()).thenReturn(requestURI); final Set<String> headerNames = Sets.newHashSet(HttpHeaders.ACCEPT); Mockito.when(request.getHeaderNames()).thenReturn(Collections.enumeration(headerNames)); Mockito.when(request.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaType.APPLICATION_JSON_VALUE); final HttpResponse forwardResponse = Mockito.mock(HttpResponse.class); final StatusLine statusLine = Mockito.mock(StatusLine.class); Mockito.when(forwardResponse.getStatusLine()).thenReturn(statusLine); final int successCode = 200; Mockito.when(statusLine.getStatusCode()).thenReturn(successCode); final Header contentTypeHeader = Mockito.mock(Header.class); Mockito.when(contentTypeHeader.getName()).thenReturn(HttpHeaders.CONTENT_TYPE); Mockito.when(contentTypeHeader.getValue()).thenReturn(MediaType.TEXT_PLAIN_VALUE); Mockito.when(forwardResponse.getAllHeaders()).thenReturn(new Header[]{contentTypeHeader}); final String text = UUID.randomUUID().toString() + UUID.randomUUID().toString() + UUID.randomUUID().toString(); final ByteArrayInputStream bis = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)); final HttpEntity entity = Mockito.mock(HttpEntity.class); Mockito.when(entity.getContent()).thenReturn(bis); Mockito.when(forwardResponse.getEntity()).thenReturn(entity); final DelegatingServletOutputStream bos = new DelegatingServletOutputStream(new ByteArrayOutputStream()); Mockito.when(response.getOutputStream()).thenReturn(bos); final ClientHttpRequestFactory factory = Mockito.mock(ClientHttpRequestFactory.class); final ClientHttpRequest clientHttpRequest = Mockito.mock(ClientHttpRequest.class); Mockito.when(clientHttpRequest.execute()) .thenReturn(new MockClientHttpResponse(text.getBytes(StandardCharsets.UTF_8), HttpStatus.OK)); Mockito.when(clientHttpRequest.getHeaders()) .thenReturn(new HttpHeaders()); Mockito.when(factory.createRequest(Mockito.any(), Mockito.any())).thenReturn(clientHttpRequest); final RestTemplate template = new RestTemplate(factory); final MeterRegistry registry = Mockito.mock(MeterRegistry.class); final Counter counter = Mockito.mock(Counter.class); Mockito.when(registry.counter(Mockito.anyString())).thenReturn(counter); final DataServices dataServices = Mockito.mock(DataServices.class); Mockito.when(dataServices.getPersistenceService()).thenReturn(this.persistenceService); final JobRestController jobController = new JobRestController( Mockito.mock(JobLaunchService.class), dataServices, this.createMockResourceAssembler(), new GenieHostInfo(this.hostname), template, this.jobDirectoryServerService, this.jobsProperties, registry, this.agentRoutingService, this.environment, Mockito.mock(AttachmentService.class), this.jobKillService ); jobController.getJobOutput(jobId, null, request, response); Assertions.assertThat(bos.getTargetStream().toString()).isEqualTo(text); Mockito.verify(request, Mockito.times(1)).getHeader(HttpHeaders.ACCEPT); Mockito.verify(this.agentRoutingService, Mockito.times(1)).getHostnameForAgentConnection(jobId); Mockito.verify(response, Mockito.never()).sendError(Mockito.anyInt()); Mockito .verify(this.jobDirectoryServerService, Mockito.never()) .serveResource( Mockito.eq(jobId), Mockito.any(URL.class), Mockito.anyString(), Mockito.eq(request), Mockito.eq(response) ); }
@GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) public void getJobOutput( @PathVariable("id") final String id, @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false) @Nullable final String forwardedFrom, final HttpServletRequest request, final HttpServletResponse response ) throws GenieException, NotFoundException { final String path = ControllerUtils.getRemainingPath(request); log.info("[getJobOutput] Called to get output path \"{}\" for job with id \"{}\"", path, id); final URL baseUrl; try { baseUrl = forwardedFrom == null ? ControllerUtils.getRequestRoot(request, path) : ControllerUtils.getRequestRoot(new URL(forwardedFrom), path); } catch (final MalformedURLException e) { throw new GenieServerException("Unable to parse base request url", e); } final ArchiveStatus archiveStatus = this.persistenceService.getJobArchiveStatus(id); if (archiveStatus == ArchiveStatus.PENDING) { final String jobHostname; try { jobHostname = this.agentRoutingService .getHostnameForAgentConnection(id) .orElseThrow(() -> new NotFoundException("No hostname found for job - " + id)); } catch (NotFoundException e) { throw new GenieServerException("Failed to route request", e); } final boolean shouldForward = !this.hostname.equals(jobHostname); final boolean canForward = forwardedFrom == null && this.jobsProperties.getForwarding().isEnabled(); if (shouldForward && canForward) { forwardRequest(id, path, jobHostname, request, response); return; } else if (!canForward && shouldForward) { throw new GenieServerException("Job files are not local, but forwarding is disabled"); } } log.debug("Fetching requested resource \"{}\" for job \"{}\"", path, id); this.jobDirectoryServerService.serveResource(id, baseUrl, path, request, response); }
JobRestController { @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) public void getJobOutput( @PathVariable("id") final String id, @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false) @Nullable final String forwardedFrom, final HttpServletRequest request, final HttpServletResponse response ) throws GenieException, NotFoundException { final String path = ControllerUtils.getRemainingPath(request); log.info("[getJobOutput] Called to get output path \"{}\" for job with id \"{}\"", path, id); final URL baseUrl; try { baseUrl = forwardedFrom == null ? ControllerUtils.getRequestRoot(request, path) : ControllerUtils.getRequestRoot(new URL(forwardedFrom), path); } catch (final MalformedURLException e) { throw new GenieServerException("Unable to parse base request url", e); } final ArchiveStatus archiveStatus = this.persistenceService.getJobArchiveStatus(id); if (archiveStatus == ArchiveStatus.PENDING) { final String jobHostname; try { jobHostname = this.agentRoutingService .getHostnameForAgentConnection(id) .orElseThrow(() -> new NotFoundException("No hostname found for job - " + id)); } catch (NotFoundException e) { throw new GenieServerException("Failed to route request", e); } final boolean shouldForward = !this.hostname.equals(jobHostname); final boolean canForward = forwardedFrom == null && this.jobsProperties.getForwarding().isEnabled(); if (shouldForward && canForward) { forwardRequest(id, path, jobHostname, request, response); return; } else if (!canForward && shouldForward) { throw new GenieServerException("Job files are not local, but forwarding is disabled"); } } log.debug("Fetching requested resource \"{}\" for job \"{}\"", path, id); this.jobDirectoryServerService.serveResource(id, baseUrl, path, request, response); } }
JobRestController { @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) public void getJobOutput( @PathVariable("id") final String id, @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false) @Nullable final String forwardedFrom, final HttpServletRequest request, final HttpServletResponse response ) throws GenieException, NotFoundException { final String path = ControllerUtils.getRemainingPath(request); log.info("[getJobOutput] Called to get output path \"{}\" for job with id \"{}\"", path, id); final URL baseUrl; try { baseUrl = forwardedFrom == null ? ControllerUtils.getRequestRoot(request, path) : ControllerUtils.getRequestRoot(new URL(forwardedFrom), path); } catch (final MalformedURLException e) { throw new GenieServerException("Unable to parse base request url", e); } final ArchiveStatus archiveStatus = this.persistenceService.getJobArchiveStatus(id); if (archiveStatus == ArchiveStatus.PENDING) { final String jobHostname; try { jobHostname = this.agentRoutingService .getHostnameForAgentConnection(id) .orElseThrow(() -> new NotFoundException("No hostname found for job - " + id)); } catch (NotFoundException e) { throw new GenieServerException("Failed to route request", e); } final boolean shouldForward = !this.hostname.equals(jobHostname); final boolean canForward = forwardedFrom == null && this.jobsProperties.getForwarding().isEnabled(); if (shouldForward && canForward) { forwardRequest(id, path, jobHostname, request, response); return; } else if (!canForward && shouldForward) { throw new GenieServerException("Job files are not local, but forwarding is disabled"); } } log.debug("Fetching requested resource \"{}\" for job \"{}\"", path, id); this.jobDirectoryServerService.serveResource(id, baseUrl, path, request, response); } @Autowired @SuppressWarnings("checkstyle:parameternumber")  JobRestController(
        final JobLaunchService jobLaunchService,
        final DataServices dataServices,
        final EntityModelAssemblers entityModelAssemblers,
        final GenieHostInfo genieHostInfo,
        @Qualifier("genieRestTemplate") final RestTemplate restTemplate,
        final JobDirectoryServerService jobDirectoryServerService,
        final JobsProperties jobsProperties,
        final MeterRegistry registry,
        final AgentRoutingService agentRoutingService,
        final Environment environment,
        final AttachmentService attachmentService,
        final JobKillService jobKillService
    ); }
JobRestController { @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) public void getJobOutput( @PathVariable("id") final String id, @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false) @Nullable final String forwardedFrom, final HttpServletRequest request, final HttpServletResponse response ) throws GenieException, NotFoundException { final String path = ControllerUtils.getRemainingPath(request); log.info("[getJobOutput] Called to get output path \"{}\" for job with id \"{}\"", path, id); final URL baseUrl; try { baseUrl = forwardedFrom == null ? ControllerUtils.getRequestRoot(request, path) : ControllerUtils.getRequestRoot(new URL(forwardedFrom), path); } catch (final MalformedURLException e) { throw new GenieServerException("Unable to parse base request url", e); } final ArchiveStatus archiveStatus = this.persistenceService.getJobArchiveStatus(id); if (archiveStatus == ArchiveStatus.PENDING) { final String jobHostname; try { jobHostname = this.agentRoutingService .getHostnameForAgentConnection(id) .orElseThrow(() -> new NotFoundException("No hostname found for job - " + id)); } catch (NotFoundException e) { throw new GenieServerException("Failed to route request", e); } final boolean shouldForward = !this.hostname.equals(jobHostname); final boolean canForward = forwardedFrom == null && this.jobsProperties.getForwarding().isEnabled(); if (shouldForward && canForward) { forwardRequest(id, path, jobHostname, request, response); return; } else if (!canForward && shouldForward) { throw new GenieServerException("Job files are not local, but forwarding is disabled"); } } log.debug("Fetching requested resource \"{}\" for job \"{}\"", path, id); this.jobDirectoryServerService.serveResource(id, baseUrl, path, request, response); } @Autowired @SuppressWarnings("checkstyle:parameternumber")  JobRestController(
        final JobLaunchService jobLaunchService,
        final DataServices dataServices,
        final EntityModelAssemblers entityModelAssemblers,
        final GenieHostInfo genieHostInfo,
        @Qualifier("genieRestTemplate") final RestTemplate restTemplate,
        final JobDirectoryServerService jobDirectoryServerService,
        final JobsProperties jobsProperties,
        final MeterRegistry registry,
        final AgentRoutingService agentRoutingService,
        final Environment environment,
        final AttachmentService attachmentService,
        final JobKillService jobKillService
    ); @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.ACCEPTED) ResponseEntity<Void> submitJob(
        @Valid @RequestBody final JobRequest jobRequest,
        @RequestHeader(value = FORWARDED_FOR_HEADER, required = false) @Nullable final String clientHost,
        @RequestHeader(value = HttpHeaders.USER_AGENT, required = false) @Nullable final String userAgent,
        final HttpServletRequest httpServletRequest
    ); @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.ACCEPTED) ResponseEntity<Void> submitJob(
        @Valid @RequestPart("request") final JobRequest jobRequest,
        @RequestPart("attachment") final MultipartFile[] attachments,
        @RequestHeader(value = FORWARDED_FOR_HEADER, required = false) @Nullable final String clientHost,
        @RequestHeader(value = HttpHeaders.USER_AGENT, required = false) @Nullable final String userAgent,
        final HttpServletRequest httpServletRequest
    ); @GetMapping(value = "/{id}", produces = MediaTypes.HAL_JSON_VALUE) EntityModel<Job> getJob(@PathVariable("id") final String id); @GetMapping(value = "/{id}/status", produces = MediaType.APPLICATION_JSON_VALUE) JsonNode getJobStatus(@PathVariable("id") final String id); @GetMapping(produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) @SuppressWarnings("checkstyle:parameternumber") PagedModel<EntityModel<JobSearchResult>> findJobs(
        @RequestParam(value = "id", required = false) @Nullable final String id,
        @RequestParam(value = "name", required = false) @Nullable final String name,
        @RequestParam(value = "user", required = false) @Nullable final String user,
        @RequestParam(value = "status", required = false) @Nullable final Set<String> statuses,
        @RequestParam(value = "tag", required = false) @Nullable final Set<String> tags,
        @RequestParam(value = "clusterName", required = false) @Nullable final String clusterName,
        @RequestParam(value = "clusterId", required = false) @Nullable final String clusterId,
        @RequestParam(value = "commandName", required = false) @Nullable final String commandName,
        @RequestParam(value = "commandId", required = false) @Nullable final String commandId,
        @RequestParam(value = "minStarted", required = false) @Nullable final Long minStarted,
        @RequestParam(value = "maxStarted", required = false) @Nullable final Long maxStarted,
        @RequestParam(value = "minFinished", required = false) @Nullable final Long minFinished,
        @RequestParam(value = "maxFinished", required = false) @Nullable final Long maxFinished,
        @RequestParam(value = "grouping", required = false) @Nullable final String grouping,
        @RequestParam(value = "groupingInstance", required = false) @Nullable final String groupingInstance,
        @PageableDefault(sort = {"created"}, direction = Sort.Direction.DESC) final Pageable page,
        final PagedResourcesAssembler<JobSearchResult> assembler
    ); @DeleteMapping(value = "/{id}") @ResponseStatus(HttpStatus.ACCEPTED) void killJob(
        @PathVariable("id") final String id,
        @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false)
        @Nullable final String forwardedFrom,
        final HttpServletRequest request,
        final HttpServletResponse response
    ); @GetMapping(value = "/{id}/request", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobRequest> getJobRequest(
        @PathVariable("id") final String id); @GetMapping(value = "/{id}/execution", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobExecution> getJobExecution(
        @PathVariable("id") final String id
    ); @GetMapping(value = "/{id}/metadata", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobMetadata> getJobMetadata(@PathVariable("id") final String id); @GetMapping(value = "/{id}/cluster", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<Cluster> getJobCluster(@PathVariable("id") final String id); @GetMapping(value = "/{id}/command", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<Command> getJobCommand(@PathVariable("id") final String id); @GetMapping(value = "/{id}/applications", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) List<EntityModel<Application>> getJobApplications(
        @PathVariable("id") final String id
    ); @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) void getJobOutput(
        @PathVariable("id") final String id,
        @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false)
        @Nullable final String forwardedFrom,
        final HttpServletRequest request,
        final HttpServletResponse response
    ); }
JobRestController { @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) public void getJobOutput( @PathVariable("id") final String id, @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false) @Nullable final String forwardedFrom, final HttpServletRequest request, final HttpServletResponse response ) throws GenieException, NotFoundException { final String path = ControllerUtils.getRemainingPath(request); log.info("[getJobOutput] Called to get output path \"{}\" for job with id \"{}\"", path, id); final URL baseUrl; try { baseUrl = forwardedFrom == null ? ControllerUtils.getRequestRoot(request, path) : ControllerUtils.getRequestRoot(new URL(forwardedFrom), path); } catch (final MalformedURLException e) { throw new GenieServerException("Unable to parse base request url", e); } final ArchiveStatus archiveStatus = this.persistenceService.getJobArchiveStatus(id); if (archiveStatus == ArchiveStatus.PENDING) { final String jobHostname; try { jobHostname = this.agentRoutingService .getHostnameForAgentConnection(id) .orElseThrow(() -> new NotFoundException("No hostname found for job - " + id)); } catch (NotFoundException e) { throw new GenieServerException("Failed to route request", e); } final boolean shouldForward = !this.hostname.equals(jobHostname); final boolean canForward = forwardedFrom == null && this.jobsProperties.getForwarding().isEnabled(); if (shouldForward && canForward) { forwardRequest(id, path, jobHostname, request, response); return; } else if (!canForward && shouldForward) { throw new GenieServerException("Job files are not local, but forwarding is disabled"); } } log.debug("Fetching requested resource \"{}\" for job \"{}\"", path, id); this.jobDirectoryServerService.serveResource(id, baseUrl, path, request, response); } @Autowired @SuppressWarnings("checkstyle:parameternumber")  JobRestController(
        final JobLaunchService jobLaunchService,
        final DataServices dataServices,
        final EntityModelAssemblers entityModelAssemblers,
        final GenieHostInfo genieHostInfo,
        @Qualifier("genieRestTemplate") final RestTemplate restTemplate,
        final JobDirectoryServerService jobDirectoryServerService,
        final JobsProperties jobsProperties,
        final MeterRegistry registry,
        final AgentRoutingService agentRoutingService,
        final Environment environment,
        final AttachmentService attachmentService,
        final JobKillService jobKillService
    ); @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.ACCEPTED) ResponseEntity<Void> submitJob(
        @Valid @RequestBody final JobRequest jobRequest,
        @RequestHeader(value = FORWARDED_FOR_HEADER, required = false) @Nullable final String clientHost,
        @RequestHeader(value = HttpHeaders.USER_AGENT, required = false) @Nullable final String userAgent,
        final HttpServletRequest httpServletRequest
    ); @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @ResponseStatus(HttpStatus.ACCEPTED) ResponseEntity<Void> submitJob(
        @Valid @RequestPart("request") final JobRequest jobRequest,
        @RequestPart("attachment") final MultipartFile[] attachments,
        @RequestHeader(value = FORWARDED_FOR_HEADER, required = false) @Nullable final String clientHost,
        @RequestHeader(value = HttpHeaders.USER_AGENT, required = false) @Nullable final String userAgent,
        final HttpServletRequest httpServletRequest
    ); @GetMapping(value = "/{id}", produces = MediaTypes.HAL_JSON_VALUE) EntityModel<Job> getJob(@PathVariable("id") final String id); @GetMapping(value = "/{id}/status", produces = MediaType.APPLICATION_JSON_VALUE) JsonNode getJobStatus(@PathVariable("id") final String id); @GetMapping(produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) @SuppressWarnings("checkstyle:parameternumber") PagedModel<EntityModel<JobSearchResult>> findJobs(
        @RequestParam(value = "id", required = false) @Nullable final String id,
        @RequestParam(value = "name", required = false) @Nullable final String name,
        @RequestParam(value = "user", required = false) @Nullable final String user,
        @RequestParam(value = "status", required = false) @Nullable final Set<String> statuses,
        @RequestParam(value = "tag", required = false) @Nullable final Set<String> tags,
        @RequestParam(value = "clusterName", required = false) @Nullable final String clusterName,
        @RequestParam(value = "clusterId", required = false) @Nullable final String clusterId,
        @RequestParam(value = "commandName", required = false) @Nullable final String commandName,
        @RequestParam(value = "commandId", required = false) @Nullable final String commandId,
        @RequestParam(value = "minStarted", required = false) @Nullable final Long minStarted,
        @RequestParam(value = "maxStarted", required = false) @Nullable final Long maxStarted,
        @RequestParam(value = "minFinished", required = false) @Nullable final Long minFinished,
        @RequestParam(value = "maxFinished", required = false) @Nullable final Long maxFinished,
        @RequestParam(value = "grouping", required = false) @Nullable final String grouping,
        @RequestParam(value = "groupingInstance", required = false) @Nullable final String groupingInstance,
        @PageableDefault(sort = {"created"}, direction = Sort.Direction.DESC) final Pageable page,
        final PagedResourcesAssembler<JobSearchResult> assembler
    ); @DeleteMapping(value = "/{id}") @ResponseStatus(HttpStatus.ACCEPTED) void killJob(
        @PathVariable("id") final String id,
        @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false)
        @Nullable final String forwardedFrom,
        final HttpServletRequest request,
        final HttpServletResponse response
    ); @GetMapping(value = "/{id}/request", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobRequest> getJobRequest(
        @PathVariable("id") final String id); @GetMapping(value = "/{id}/execution", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobExecution> getJobExecution(
        @PathVariable("id") final String id
    ); @GetMapping(value = "/{id}/metadata", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<JobMetadata> getJobMetadata(@PathVariable("id") final String id); @GetMapping(value = "/{id}/cluster", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<Cluster> getJobCluster(@PathVariable("id") final String id); @GetMapping(value = "/{id}/command", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) EntityModel<Command> getJobCommand(@PathVariable("id") final String id); @GetMapping(value = "/{id}/applications", produces = MediaTypes.HAL_JSON_VALUE) @ResponseStatus(HttpStatus.OK) List<EntityModel<Application>> getJobApplications(
        @PathVariable("id") final String id
    ); @GetMapping( value = { "/{id}/output", "/{id}/output/", "/{id}/output/**" } ) void getJobOutput(
        @PathVariable("id") final String id,
        @RequestHeader(name = JobConstants.GENIE_FORWARDED_FROM_HEADER, required = false)
        @Nullable final String forwardedFrom,
        final HttpServletRequest request,
        final HttpServletResponse response
    );  }