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

au.csiro.pathling.fhir.ConformanceProvider Maven / Gradle / Ivy

There is a newer version: 7.0.1
Show newest version
/*
 * 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.fhir;

import static au.csiro.pathling.security.OidcConfiguration.ConfigItem.AUTH_URL;
import static au.csiro.pathling.security.OidcConfiguration.ConfigItem.REVOKE_URL;
import static au.csiro.pathling.security.OidcConfiguration.ConfigItem.TOKEN_URL;
import static au.csiro.pathling.utilities.Preconditions.checkPresent;
import static au.csiro.pathling.utilities.Preconditions.checkUserInput;
import static au.csiro.pathling.utilities.Versioning.getMajorVersion;
import static java.util.Objects.requireNonNull;

import au.csiro.pathling.PathlingVersion;
import au.csiro.pathling.caching.Cacheable;
import au.csiro.pathling.config.ServerConfiguration;
import au.csiro.pathling.errors.ResourceNotFoundError;
import au.csiro.pathling.security.OidcConfiguration;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import com.google.common.collect.ImmutableMap;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementImplementationComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementKind;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceOperationComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestSecurityComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementSoftwareComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.ResourceInteractionComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.RestfulCapabilityMode;
import org.hl7.fhir.r4.model.CapabilityStatement.SystemInteractionComponent;
import org.hl7.fhir.r4.model.CapabilityStatement.SystemRestfulInteraction;
import org.hl7.fhir.r4.model.CapabilityStatement.TypeRestfulInteraction;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Enumerations.FHIRVersion;
import org.hl7.fhir.r4.model.Enumerations.PublicationStatus;
import org.hl7.fhir.r4.model.Enumerations.ResourceType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.OperationDefinition;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/**
 * This class provides a customised CapabilityStatement describing the functionality of the
 * analytics server.
 *
 * @author John Grimes
 * @see CapabilityStatement
 */
@Component
@Profile("server")
@Slf4j
public class ConformanceProvider implements IServerConformanceProvider,
    Cacheable {

  /**
   * The base URI for canonical URIs.
   */
  public static final String URI_BASE = "https://pathling.csiro.au/fhir";
  /**
   * All system-level operations available within Pathling.
   */
  public static final List SYSTEM_LEVEL_OPERATIONS = Arrays.asList("import", "result",
      "job");

  private static final String FHIR_RESOURCE_BASE = "http://hl7.org/fhir/StructureDefinition/";
  private static final String RESTFUL_SECURITY_URI = "http://terminology.hl7.org/CodeSystem/restful-security-service";
  private static final String RESTFUL_SECURITY_CODE = "SMART-on-FHIR";
  private static final String SMART_OAUTH_URI = "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris";
  private static final String UNKNOWN_VERSION = "UNKNOWN";

  /**
   * All resource-level operations available within Pathling.
   */
  private static final List RESOURCE_LEVEL_OPERATIONS = Arrays.asList("aggregate",
      "search", "extract");

  /**
   * All operations available within Pathling.
   */
  private static final List OPERATIONS;

  static {
    OPERATIONS = new ArrayList<>();
    OPERATIONS.addAll(ConformanceProvider.SYSTEM_LEVEL_OPERATIONS);
    OPERATIONS.addAll(ConformanceProvider.RESOURCE_LEVEL_OPERATIONS);
  }

  @Nonnull
  private final ServerConfiguration configuration;

  @Nonnull
  private final Optional oidcConfiguration;

  @Nonnull
  private Optional restfulServer;

  @Nonnull
  private final PathlingVersion version;

  @Nonnull
  private final FhirContext fhirContext;

  @Nonnull
  private final IParser jsonParser;

  @Nonnull
  private final Map resources;

  /**
   * @param configuration a {@link ServerConfiguration} object controlling the behaviour of the
   * capability statement
   * @param oidcConfiguration a {@link OidcConfiguration} object containing configuration retrieved
   * from OIDC discovery
   * @param version a {@link PathlingVersion} object containing version information for the server
   * @param fhirContext a {@link FhirContext} for determining the supported FHIR version
   * @param jsonParser a {@link IParser} for parsing JSON OperationDefinitions
   */
  public ConformanceProvider(@Nonnull final ServerConfiguration configuration,
      @Nonnull final Optional oidcConfiguration,
      @Nonnull final PathlingVersion version, @Nonnull final FhirContext fhirContext,
      @Nonnull final IParser jsonParser) {
    this.configuration = configuration;
    this.oidcConfiguration = oidcConfiguration;
    this.version = version;
    this.fhirContext = fhirContext;
    this.jsonParser = jsonParser;

    final ImmutableMap.Builder mapBuilder = new ImmutableMap.Builder<>();
    for (final String operation : ConformanceProvider.OPERATIONS) {
      final String id =
          "OperationDefinition/" + operation + "-" + getMajorVersion(
              version.getMajorVersion().orElse("UNKNOWN"));
      final String path = "fhir/" + operation + ".OperationDefinition.json";
      mapBuilder.put(id, load(path));
    }
    resources = mapBuilder.build();

    restfulServer = Optional.empty();
  }

  @Override
  @Metadata(cacheMillis = 0)
  public CapabilityStatement getServerConformance(
      @Nullable final HttpServletRequest httpServletRequest,
      @Nullable final RequestDetails requestDetails) {
    log.debug("Received request for server capabilities");

    final CapabilityStatement capabilityStatement = new CapabilityStatement();
    capabilityStatement.setUrl(getCapabilityUri());
    capabilityStatement.setVersion(version.getBuildVersion().orElse(UNKNOWN_VERSION));
    capabilityStatement.setTitle("Pathling FHIR API");
    capabilityStatement.setName("pathling-fhir-api");
    capabilityStatement.setStatus(PublicationStatus.ACTIVE);
    capabilityStatement.setExperimental(true);
    capabilityStatement.setPublisher("Australian e-Health Research Centre, CSIRO");
    capabilityStatement.setCopyright(
        "Dedicated to the public domain via CC0: https://creativecommons.org/publicdomain/zero/1.0/");
    capabilityStatement.setKind(CapabilityStatementKind.INSTANCE);

    final CapabilityStatementSoftwareComponent software = new CapabilityStatementSoftwareComponent(
        new StringType("Pathling"));
    software.setVersion(version.getDescriptiveVersion().orElse(UNKNOWN_VERSION));
    capabilityStatement.setSoftware(software);

    final CapabilityStatementImplementationComponent implementation =
        new CapabilityStatementImplementationComponent(
            new StringType(configuration.getImplementationDescription()));
    final Optional serverBase = getServerBase(Optional.ofNullable(httpServletRequest));
    serverBase.ifPresent(implementation::setUrl);
    capabilityStatement.setImplementation(implementation);

    final FHIRVersion fhirVersion = FHIRVersion.fromCode(
        fhirContext.getVersion().getVersion().getFhirVersionString());
    capabilityStatement.setFhirVersion(fhirVersion);
    capabilityStatement.setFormat(Arrays.asList(new CodeType("json"), new CodeType("xml")));
    capabilityStatement.setRest(buildRestComponent());

    return capabilityStatement;
  }

  @Nonnull
  private List buildRestComponent() {
    final List rest = new ArrayList<>();
    final CapabilityStatementRestComponent server = new CapabilityStatementRestComponent();
    server.setMode(RestfulCapabilityMode.SERVER);
    server.setSecurity(buildSecurity());
    server.setResource(buildResources());
    server.setOperation(buildOperations());
    server.setInteraction(buildSystemLevelInteractions());
    rest.add(server);
    return rest;
  }

  @Nonnull
  private CapabilityStatementRestSecurityComponent buildSecurity() {
    final CapabilityStatementRestSecurityComponent security = new CapabilityStatementRestSecurityComponent();
    security.setCors(true);
    if (configuration.getAuth().isEnabled()) {
      final OidcConfiguration checkedConfig = checkPresent(oidcConfiguration);

      final CodeableConcept smart = new CodeableConcept(
          new Coding(RESTFUL_SECURITY_URI, RESTFUL_SECURITY_CODE, RESTFUL_SECURITY_CODE));
      smart.setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)");
      security.getService().add(smart);

      final Optional authUrl = checkedConfig.get(AUTH_URL);
      final Optional tokenUrl = checkedConfig.get(TOKEN_URL);
      final Optional revokeUrl = checkedConfig.get(REVOKE_URL);
      if (authUrl.isPresent() || tokenUrl.isPresent() || revokeUrl.isPresent()) {
        final Extension oauthUris = new Extension(SMART_OAUTH_URI);
        authUrl.ifPresent(url -> oauthUris.addExtension("authorize", new UriType(url)));
        tokenUrl.ifPresent(url -> oauthUris.addExtension("token", new UriType(url)));
        revokeUrl.ifPresent(url -> oauthUris.addExtension("revoke", new UriType(url)));
        security.addExtension(oauthUris);
      }
    }
    return security;
  }

  @Nonnull
  private List buildResources() {
    final List resources = new ArrayList<>();
    final Set supported = FhirServer.supportedResourceTypes();
    final Set supportedResourceTypes = supported.isEmpty()
                                                     ? EnumSet.noneOf(ResourceType.class)
                                                     : EnumSet.copyOf(supported);

    for (final ResourceType resourceType : supportedResourceTypes) {
      final CapabilityStatementRestResourceComponent resource =
          new CapabilityStatementRestResourceComponent(new CodeType(resourceType.toCode()));
      resource.setProfile(FHIR_RESOURCE_BASE + resourceType.toCode());

      // Add the search operation to all resources.
      final ResourceInteractionComponent search = new ResourceInteractionComponent();
      search.setCode(TypeRestfulInteraction.SEARCHTYPE);
      resource.getInteraction().add(search);

      // Add the create and update operations to all resources.
      final ResourceInteractionComponent create = new ResourceInteractionComponent();
      final ResourceInteractionComponent update = new ResourceInteractionComponent();
      create.setCode(TypeRestfulInteraction.CREATE);
      update.setCode(TypeRestfulInteraction.UPDATE);
      resource.getInteraction().add(create);
      resource.getInteraction().add(update);

      // Add the `aggregate` operation to all resources.
      final CanonicalType aggregateOperationUri = new CanonicalType(getOperationUri("aggregate"));
      final CapabilityStatementRestResourceOperationComponent aggregateOperation =
          new CapabilityStatementRestResourceOperationComponent(new StringType("aggregate"),
              aggregateOperationUri);
      resource.addOperation(aggregateOperation);

      // Add the `extract` operation to all resources.
      final CanonicalType extractOperationUri = new CanonicalType(getOperationUri("extract"));
      final CapabilityStatementRestResourceOperationComponent extractOperation =
          new CapabilityStatementRestResourceOperationComponent(new StringType("extract"),
              extractOperationUri);
      resource.addOperation(extractOperation);

      // Add the `fhirPath` search parameter to all resources.
      final CapabilityStatementRestResourceOperationComponent searchOperation = new CapabilityStatementRestResourceOperationComponent();
      searchOperation.setName("fhirPath");
      searchOperation.setDefinition(getOperationUri("search"));
      resource.addOperation(searchOperation);

      resources.add(resource);
    }

    // Add the read operation to the OperationDefinition resource.
    final String opDefCode = ResourceType.OPERATIONDEFINITION.toCode();
    final CapabilityStatementRestResourceComponent opDefResource =
        new CapabilityStatementRestResourceComponent(new CodeType(opDefCode));
    opDefResource.setProfile(FHIR_RESOURCE_BASE + opDefCode);
    final ResourceInteractionComponent readInteraction = new ResourceInteractionComponent();
    readInteraction.setCode(TypeRestfulInteraction.READ);
    opDefResource.addInteraction(readInteraction);
    resources.add(opDefResource);

    return resources;
  }

  @Nonnull
  private List buildOperations() {
    final List operations = new ArrayList<>();

    for (final String name : SYSTEM_LEVEL_OPERATIONS) {
      final CanonicalType operationUri = new CanonicalType(getOperationUri(name));
      final CapabilityStatementRestResourceOperationComponent operation =
          new CapabilityStatementRestResourceOperationComponent(new StringType(name),
              operationUri);
      operations.add(operation);
    }

    return operations;
  }

  @Nonnull
  private List buildSystemLevelInteractions() {
    final List interactions = new ArrayList<>();
    final SystemInteractionComponent interaction = new SystemInteractionComponent();
    interaction.setCode(SystemRestfulInteraction.BATCH);
    interactions.add(interaction);
    return interactions;
  }

  @Nonnull
  private String getCapabilityUri() {
    return URI_BASE + "/CapabilityStatement/pathling-fhir-api-" + version.getMajorVersion()
        .orElse(UNKNOWN_VERSION);
  }

  @Nonnull
  private String getOperationUri(final String name) {
    return URI_BASE + "/OperationDefinition/" + name + "-" + version.getMajorVersion()
        .orElse(UNKNOWN_VERSION);
  }

  @Override
  public void setRestfulServer(@Nullable final RestfulServer restfulServer) {
    this.restfulServer = Optional.ofNullable(restfulServer);
  }

  @Nonnull
  private Optional getServerBase(
      @Nonnull final Optional httpServletRequest) {
    if (httpServletRequest.isEmpty() || restfulServer.isEmpty()) {
      log.warn("Attempted to get server base URL, HTTP servlet request or RestfulServer missing");
      return Optional.empty();
    } else {
      final ServletContext servletContext =
          (ServletContext) httpServletRequest.get()
              .getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE);
      return Optional.ofNullable(restfulServer.get().getServerAddressStrategy()
          .determineServerBase(servletContext, httpServletRequest.get()));
    }
  }

  @Override
  public Optional getCacheKey() {
    return version.getDescriptiveVersion();
  }

  @Override
  public boolean cacheKeyMatches(final String otherKey) {
    return version.getDescriptiveVersion().map(key -> key.equals(otherKey)).orElse(false);
  }

  /**
   * Handles all read requests to the OperationDefinition resource.
   *
   * @param id the ID of the desired OperationDefinition
   * @return an {@link OperationDefinition} resource
   */
  @Read(typeName = "OperationDefinition")
  @SuppressWarnings("unused")
  public IBaseResource getOperationDefinitionById(@Nullable @IdParam final IIdType id) {
    checkUserInput(id != null, "Missing ID parameter");
    log.info("Reading OperationDefinition with ID {}", id.getValue());

    final String idString = id.getValue();
    final OperationDefinition resource = resources.get(idString);
    if (resource == null) {
      throw new ResourceNotFoundError("OperationDefinition not found: " + idString);
    }
    return resource;
  }

  @Nonnull
  private OperationDefinition load(@Nonnull final String resourcePath) {
    @Nullable final InputStream resourceStream = Thread.currentThread().getContextClassLoader()
        .getResourceAsStream(resourcePath);
    requireNonNull(resourceStream);

    final OperationDefinition operationDefinition = (OperationDefinition) jsonParser
        .parseResource(resourceStream);
    final String id = String
        .format("%1$s%2$s", operationDefinition.getName(),
            version.getMajorVersion().map(v -> String.format("-%1$s", v)).orElse(""));
    operationDefinition.setId(id);
    final String url = String
        .format("%1$s/OperationDefinition/%2$s", ConformanceProvider.URI_BASE, id);
    operationDefinition.setUrl(url);
    operationDefinition.setVersion(version.getBuildVersion().orElse(
        ConformanceProvider.UNKNOWN_VERSION));
    return operationDefinition;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy