All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.csiro.pathling.async.JobProvider Maven / Gradle / Ivy

/*
 * Copyright 2023 Commonwealth Scientific and Industrial Research
 * Organisation (CSIRO) ABN 41 687 119 230.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package au.csiro.pathling.async;

import static au.csiro.pathling.caching.EntityTagInterceptor.makeRequestNonCacheable;
import static au.csiro.pathling.caching.EntityTagInterceptor.requestIsCacheable;
import static au.csiro.pathling.security.SecurityAspect.checkHasAuthority;
import static au.csiro.pathling.security.SecurityAspect.getCurrentUserId;
import static java.util.Objects.requireNonNull;

import au.csiro.pathling.config.ServerConfiguration;
import au.csiro.pathling.errors.AccessDeniedError;
import au.csiro.pathling.errors.ErrorHandlingInterceptor;
import au.csiro.pathling.errors.ResourceNotFoundError;
import au.csiro.pathling.security.PathlingAuthority;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r4.model.Parameters;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

/**
 * @author John Grimes
 */
@Component
@Profile("server")
@ConditionalOnProperty(prefix = "pathling", name = "async.enabled", havingValue = "true")
@Slf4j
public class JobProvider {


  // regex for UUID
  private static final Pattern ID_PATTERN = Pattern.compile(
      "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$");
  private static final String PROGRESS_HEADER = "X-Progress";

  @Nonnull
  private final ServerConfiguration configuration;

  @Nonnull
  private final JobRegistry jobRegistry;

  /**
   * @param configuration a {@link ServerConfiguration} for determining if authorization is enabled
   * @param jobRegistry the {@link JobRegistry} used to keep track of running jobs
   */
  public JobProvider(@Nonnull final ServerConfiguration configuration,
      @Nonnull final JobRegistry jobRegistry) {
    this.configuration = configuration;
    this.jobRegistry = jobRegistry;
  }

  /**
   * Queries a running job for its progress, completion status and final result.
   *
   * @param id the ID of the running job
   * @param request the {@link HttpServletRequest} for checking its cacheability
   * @param response the {@link HttpServletResponse} for updating the response
   * @return the final result of the job, as a {@link Parameters} resource
   */
  @SuppressWarnings({"unused", "TypeMayBeWeakened"})
  @Operation(name = "$job", idempotent = true)
  public IBaseResource job(@Nullable @OperationParam(name = "id") final String id,
      @Nullable final HttpServletRequest request,
      @Nullable final HttpServletResponse response) {

    // Validate that the ID looks reasonable.
    if (id == null || !ID_PATTERN.matcher(id).matches()) {
      throw new ResourceNotFoundError("Job ID not found");
    }

    log.debug("Received request to check job status: {}", id);
    @Nullable final Job job = jobRegistry.get(id);
    // Check that the job exists.
    if (job == null) {
      throw new ResourceNotFoundError("Job ID not found");
    }

    if (configuration.getAuth().isEnabled()) {
      // Check for the required authority associated with the operation that initiated the job.
      checkHasAuthority(PathlingAuthority.operationAccess(job.getOperation()));
      // Check that the user requesting the job status is the same user that started the job.
      final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      final Optional currentUserId = getCurrentUserId(authentication);
      if (!job.getOwnerId().equals(currentUserId)) {
        throw new AccessDeniedError("The requested job is not owned by the current user");
      }
    }

    if (job.getResult().isDone()) {
      // If the job is done, we return the Parameters resource.
      try {
        return job.getResult().get();
      } catch (final InterruptedException e) {
        throw new InternalErrorException("Job was interrupted", e);
      } catch (final ExecutionException e) {
        // This needs to go down two levels of causes: one for the ExecutionException added by the
        // Future, and another one for the RuntimeException added by the AsyncAspect.
        throw ErrorHandlingInterceptor.convertError(e.getCause().getCause());
      }
    } else {
      // If the job is not done, we return a 202 along with an OperationOutcome and progress header.
      requireNonNull(response);
      // We need to set the caching headers such that the incomplete response is never cached.
      if (request != null && requestIsCacheable(request)) {
        makeRequestNonCacheable(response, configuration);
      }
      // Add progress information to the response.
      if (job.getTotalStages() > 0) {
        final int progress = job.getProgressPercentage();
        if (progress != 100) {
          // We don't bother showing 100%, this usually means that there are outstanding stages
          // which have not yet been submitted.
          response.setHeader(PROGRESS_HEADER, progress + "%");
        }
      }
      throw new ProcessingNotCompletedException("Processing", buildProcessingOutcome());
    }
  }

  @Nonnull
  private static OperationOutcome buildProcessingOutcome() {
    final OperationOutcome opOutcome = new OperationOutcome();
    final OperationOutcomeIssueComponent issue = new OperationOutcomeIssueComponent();
    issue.setCode(IssueType.INFORMATIONAL);
    issue.setSeverity(IssueSeverity.INFORMATION);
    issue.setDiagnostics("Job currently processing");
    opOutcome.addIssue(issue);
    return opOutcome;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy