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

org.hl7.fhir.r5.conformance.CapabilityStatementUtilities Maven / Gradle / Ivy

package org.hl7.fhir.r5.conformance;

import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.extensions.ExtensionConstants;
import org.hl7.fhir.r5.model.*;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.utilities.Utilities;

import java.util.*;

public class CapabilityStatementUtilities {
  private IWorkerContext context;

  public CapabilityStatementUtilities(IWorkerContext context) {
    this.context = context;
  }

  /*
   * Resolves any imported CapabilityStatements and returns a revised CapabilityStatement that merges all functionality from
   * the imported CapabilityStatements
   * @param targetCS - The CapabilityStatement (potentially) containing imports to be resolved
   * @throws FHIRException - If there's an issue resolving any of the imports
   */
  public CapabilityStatement resolveImports(CapabilityStatement targetCS) throws FHIRException {
    CapabilityStatement resolvedCS = targetCS.copy();
    return resolveImports(resolvedCS, new HashMap<>(), "SHALL");
  }

  /*
   * Resolves any imported CapabilityStatements and returns a revised CapabilityStatement that merges all functionality from
   * the imported CapabilityStatements
   * @param targetCS - The CapabilityStatement (potentially) containing imports to be resolved
   * @param importedUrls - Keeps track of what CapabilityStatements have already been merged so that if the same CS appears more
   *    than once in the hierarchy, we only process it once.  Also keeps track of what 'strength' the import is.
   * @throws FHIRException - If there's an issue resolving any of the imports
   *
   * When processing imports, an imported CapabilityStatement can itself declare a conformance expectation.  If an import is a SHOULD or a MAY,
   * then even if the imported CS asserts something as a SHALL, the highest effective level of conformance for the imported statements is the
   * conformance level for the import itself.  And that cascades.  So if a MAY import points to a SHOULD import, the max conformance is 'MAY'.
   *
   * The merge process also tackles most of the semantically-significant extensions on CababilityStatement.
   *
   * Metadata is not merged - so things like description, publisher, etc. are taken only from the root importing CS.
   */
  public CapabilityStatement resolveImports(CapabilityStatement targetCS, Map importedUrls, String conformance) throws FHIRException {
    if (!targetCS.hasImports())
      return targetCS;

    CapabilityStatement resolvedCS = targetCS.copy();
    for (CanonicalType canonical: resolvedCS.getImports()) {
      CapabilityStatement importedCS = context.fetchResource(CapabilityStatement.class, canonical.getValue());
      if (importedCS == null)
        throw new FHIRException("Unable to resolve CapabilityStatement " + canonical.getValue() + " imported by " + targetCS.getUrl());
      String importConformance = effectiveConformance(canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT), conformance);
      if (importedUrls.containsKey(canonical.getValue()) && !importedUrls.get(canonical.getValue()).equals(importConformance)) {
        throw new FHIRException("The CapabilityStatement " + canonical.getValue() + " is imported with different strengths - " + importedUrls.get(canonical.getValue()).equals(importConformance) + ", " + importConformance +
          (importConformance.equals(canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT)) ? "" : "(effective from " + canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT) + ")"));
      }
      importedUrls.put(targetCS.getUrl(), importConformance);

      CapabilityStatement mergedImportedCS = resolveImports(importedCS, importedUrls, importConformance);
      mergeCS(resolvedCS, mergedImportedCS, importConformance);
    }

    return resolvedCS;
  }

  /*
   * Merges the details of an imported capability statement into the 'target' capability statement (which is a copy of the importing CS)
   * The general import rules are as follows:
   * - If the importing CS has something and the imported doesn't, grab what the importing does.
   * - If the imported CS has something and the importing doesn't, grab what the imported does.
   * - If both do something, combine the functionality and set the conformance expectation to the highest of the two
   *
   * Additional rules:
   *  - CS allows you to specify separate messaging repetitions with different combinations of recipients.  That gets miserable to try to merge
   *    because the potential recipients can be overlapping.  It's also super-rare to do that.  So this algorithm only handles merging if there's
   *    a maximum of one messaging element (though that one element can list lots of supported messages)
   *  - If there's non-repeating elements with different values, grab the one with the highest conformance expectation.  If the conformance levels
   *    are the same, then fail due to the conflict.  For example, if one says SHOULD security.cors = true and the other says SHALL security.cors = false,
   *    then the SHALL takes precedence.  On the other hand, if both say SHOULD with different values, that's conflict and will trigger an exception.
   *  - For certain 'coded' elements there's a hierarchy.  versioned-update implies versioned.  So if you have SHOULD:versioned and SHOULD:versioned-update,
   *    that's *not* a conflict and you'll end up with SHOULD:versioned-update.  (Hierarchies are handled by the weightForEnum function.)
   *  - For numeric values, will take the strictest.  So for timeout values, if there is a SHOULD:5seconds and SHOULD:10 seconds, you'll get SHOULD:5 seconds
   *  - For coded values, a match means all codings match (code, system, and version if present) and text matches.  Display names are ignored
   *  - It's also a conflict if:
   *    - the same operation 'code' is associated with different operation definitions
   *    - the same search parameter 'code' is associated with different search parameter definitions
   *    - the same rest.resource is tied to different profiles.  (We could try to be smart and figure out if the imported profile is a proper subset of the
   *      importing profile, but that was too hard to take on at this point)
   *    - An imported search combination has more 'optional' elements than the importing search combination
   *  - The following are additional limitations
   *    - Can't handle endpoints on an imported CS.  (That's a super-weird situation and couldn't decide what to do about it.)  Same is true for importing
   *      a CS with messages that declare endpoints
   *
   *
   */
  protected void mergeCS(CapabilityStatement targetCS, CapabilityStatement importedCS, String maxConformance) {
    merge(targetCS.getFormat(), importedCS.getFormat(), maxConformance, "format");
    merge(targetCS.getPatchFormat(), importedCS.getPatchFormat(), maxConformance, "patchFormat");
    merge(targetCS.getAcceptLanguage(), importedCS.getAcceptLanguage(), maxConformance, "acceptLanguage");
    merge(targetCS.getImplementationGuide(), importedCS.getImplementationGuide(), maxConformance, "implementationGuide");
    merge(targetCS.getRest(), importedCS.getRest(), maxConformance, "rest");
    if (targetCS.getMessaging().size()>1)
      throw new FHIRException("Unable to handle messaging repetitions greater than one for importing Capability Statement - use one repetition with multiple messaging.supportedMessage elements.");
    else if (importedCS.getMessaging().size()>1)
      throw new FHIRException("Unable to handle messaging repetitions greater than one for imported Capability Statement - use one repetition with multiple messaging.supportedMessage elements.");
    else if (!importedCS.hasMessaging()) {
      // Do nothing
    } else if (!targetCS.hasMessaging())
      targetCS.setMessaging(importedCS.getMessaging());
    else {
      CapabilityStatement.CapabilityStatementMessagingComponent targetMessaging = targetCS.getMessaging().get(0);
      CapabilityStatement.CapabilityStatementMessagingComponent importedMessaging = importedCS.getMessaging().get(0);
      merge(targetMessaging.getReliableCacheElement(), importedMessaging.getReliableCacheElement(), maxConformance, "messaging.reliableCache");
      if (importedMessaging.hasEndpoint())
        throw new FHIRException("Importing capability statements that assert endpoints is not supported");
      merge(targetMessaging.getSupportedMessage(), importedMessaging.getSupportedMessage(), maxConformance, "messaging.supportedMessage");
    }
    merge(targetCS.getMessaging(), importedCS.getMessaging(), maxConformance, "messaging");
    merge(targetCS.getDocument(), importedCS.getDocument(), maxConformance, "messaging");
  }

  void mergeProperties(CapabilityStatement.CapabilityStatementRestComponent targetType, CapabilityStatement.CapabilityStatementRestComponent importedType, String maxConformance, String context) {
    String localContext = context + "." + targetType.getMode();

    merge(targetType.getExtensionsByUrl(ExtensionConstants.EXT_CSDECLARED_PROFILE), importedType.getExtensionsByUrl(ExtensionConstants.EXT_CSDECLARED_PROFILE), maxConformance, ".extension(DeclaredProfile)");
    merge(targetType.getExtensionsByUrl(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION), importedType.getExtensionsByUrl(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION), maxConformance, ".extension(SearchMode)");
    if (!targetType.hasSecurity())
      targetType.setSecurity(importedType.getSecurity());
    else if (!importedType.hasSecurity())
      return;
    else {
      mergeProperties(targetType.getSecurity(), importedType.getSecurity(), maxConformance, localContext);
      mergeExpectations(targetType.getSecurity(), importedType.getSecurity(), maxConformance);
    }
    merge(targetType.getResource(), importedType.getResource(), maxConformance, localContext + ".resource");
    merge(targetType.getInteraction(), importedType.getInteraction(), maxConformance, localContext + ".interaction");
    merge(targetType.getOperation(), importedType.getOperation(), maxConformance, localContext + ".operation");
    merge(targetType.getSearchParam(), importedType.getSearchParam(), maxConformance, localContext + ".searchParam");
  }

  /*
   * Merges the properties of two RestSecurity components together
   * NOTE: Doesn't merge documentation or extensions
   */
  // TODO: Handle known security extensions
  void mergeProperties(CapabilityStatement.CapabilityStatementRestSecurityComponent targetType, CapabilityStatement.CapabilityStatementRestSecurityComponent importedType, String maxConformance, String context) {
    merge(targetType.getCorsElement(), importedType.getCorsElement(), maxConformance, context + ".cors");
    merge(targetType.getService(), importedType.getService(), maxConformance, context + ".service");
  }

  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceComponent targetType, CapabilityStatement.CapabilityStatementRestResourceComponent importedType, String maxConformance, String context) throws FHIRException {
    String localContext = context + "." + targetType.getType();
    if (targetType.hasProfile() && importedType.hasProfile() && !targetType.getProfile().equals(importedType.getProfile()))
      throw new FHIRException("Conflicting resource profiles for " + localContext + ".  If both the importing and imported CapabilityStatement declare profiles for the same resource, those profiles must be the same." +
        "Importing: " + targetType.getProfile() + "; Imported: " + importedType.getProfile());
    merge(targetType.getSupportedProfile(), importedType.getSupportedProfile(), maxConformance, localContext + ".supportedProfile");
    targetType.setVersioningElement(merge(targetType.getVersioningElement(), importedType.getVersioningElement(), maxConformance, localContext + ".versioning"));
    merge(targetType.getInteraction(), importedType.getInteraction(), maxConformance, localContext + ".interaction");
    targetType.setReadHistoryElement(merge(targetType.getReadHistoryElement(), importedType.getReadHistoryElement(), maxConformance, localContext + ".readHistory"));
    targetType.setUpdateCreateElement(merge(targetType.getUpdateCreateElement(), importedType.getUpdateCreateElement(), maxConformance, localContext + ".updateCreate"));
    targetType.setConditionalCreateElement(merge(targetType.getConditionalCreateElement(), importedType.getConditionalCreateElement(), maxConformance, localContext + ".conditionalCreate"));
    targetType.setConditionalReadElement(merge(targetType.getConditionalReadElement(), importedType.getConditionalReadElement(), maxConformance, localContext + ".conditionalRead"));
    targetType.setConditionalPatchElement(merge(targetType.getConditionalPatchElement(), importedType.getConditionalPatchElement(), maxConformance, localContext + ".conditionalPatch"));
    targetType.setConditionalDeleteElement(merge(targetType.getConditionalDeleteElement(), importedType.getConditionalDeleteElement(), maxConformance, localContext + ".conditionalDelete"));
    merge(targetType.getReferencePolicy(), importedType.getReferencePolicy(), maxConformance, localContext + ".referencePolicy");
    merge(targetType.getSearchInclude(), importedType.getSearchInclude(), maxConformance, localContext + ".searchInclude");
    merge(targetType.getSearchRevInclude(), importedType.getSearchRevInclude(), maxConformance, localContext + ".searchRevInclude");
    merge(targetType.getSearchParam(), importedType.getSearchParam(), maxConformance, localContext + ".searchParam");
    merge(targetType.getOperation(), importedType.getOperation(), maxConformance, localContext + ".operation");
  }

  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceOperationComponent targetType, CapabilityStatement.CapabilityStatementRestResourceOperationComponent importedType, String context) throws FHIRException {
    String localContext = context + "(name=" + targetType.getName() + ")";
    if (!importedType.hasDefinition()) {
      // do nothing
    } else if (!targetType.hasDefinition())
      targetType.setDefinitionElement(importedType.getDefinitionElement());
    else if (!targetType.getDefinition().equals(importedType.getDefinition()))
      throw new FHIRException("Differing definitions for same operation " + localContext + " in imported IG.  Importing:" + targetType.getDefinition() + "; imported:" + importedType.getDefinition());
  }

  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent targetType, CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent importedType, String maxConformance, String context) throws FHIRException {
    String localContext = context + "(name=" + targetType.getName() + ")";
    if (!importedType.hasDefinition()) {
      // do nothing
    } else if (!targetType.hasDefinition()) {
      targetType.setDefinitionElement((CanonicalType)fixMax(importedType.getDefinitionElement(), maxConformance));
    } else if (!targetType.getDefinition().equals(importedType.getDefinition()))
      throw new FHIRException("Differing definitions for same Search parameter " + localContext + " in imported IG.  Importing:" + targetType.getDefinition() + "; imported:" + importedType.getDefinition());
    if (!importedType.hasType()) {
      // do nothing
    } else if (!targetType.hasType()) {
      targetType.setTypeElement((Enumeration)fixMax(importedType.getTypeElement(), maxConformance));
    } else if (!targetType.getType().equals(importedType.getType()))
      throw new FHIRException("Differing search types for same Search parameter " + localContext + " in imported IG.  Importing:" + targetType.getType() + "; imported:" + importedType.getType());
  }

  void mergeProperties(CapabilityStatement.CapabilityStatementMessagingComponent targetType, CapabilityStatement.CapabilityStatementMessagingComponent importedType, String maxConformance, String context) throws FHIRException {
    if (importedType.hasEndpoint()) {
      throw new FHIRException("Cannot handle importing messaging with declared endpoints");
    }
    targetType.setReliableCacheElement(merge(targetType.getReliableCacheElement(), importedType.getReliableCacheElement(), maxConformance, context + ".reliableCache"));
    merge(targetType.getSupportedMessage(), importedType.getSupportedMessage(), maxConformance, context + ".reliableCache");
  }

  void merge(List targetList, List importedList, String context) throws FHIRException {
    merge(targetList, importedList, "SHALL", context);
  }

  /*
   * Combines any 'simple' types found in the 'imported' list into the target list, merging conformance expectations found on matching codes
   */
  void merge(List targetList, List importedList, String maxConformance, String context) throws FHIRException {
    for (Object importedType : importedList) {
      Object foundType = null;
      for (Object targetType : targetList) {
        boolean match;
        if (targetType instanceof PrimitiveType)
          match = importedType.toString().equals(targetType.toString());
        else if (importedType instanceof CodeableConcept)
          match = match((CodeableConcept)targetType,(CodeableConcept)importedType);
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestComponent)
          match = ((CapabilityStatement.CapabilityStatementRestComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementRestComponent)importedType).getMode());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceComponent)
          match = ((CapabilityStatement.CapabilityStatementRestResourceComponent)targetType).getType().equals(((CapabilityStatement.CapabilityStatementRestResourceComponent)importedType).getType());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceOperationComponent)
          match = ((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)targetType).getName().equals(((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)importedType).getName());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)
          match = ((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)targetType).getName().equals(((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)importedType).getName());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingComponent)
          match = true; // We only work if there's only one messaging component in each
        else if (importedType instanceof CapabilityStatement.ResourceInteractionComponent)
          match = ((CapabilityStatement.ResourceInteractionComponent)targetType).getCode().equals(((CapabilityStatement.ResourceInteractionComponent)importedType).getCode());
        else if (importedType instanceof CapabilityStatement.SystemInteractionComponent)
          match = ((CapabilityStatement.SystemInteractionComponent)targetType).getCode().equals(((CapabilityStatement.SystemInteractionComponent)importedType).getCode());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementDocumentComponent)
          match = ((CapabilityStatement.CapabilityStatementDocumentComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementDocumentComponent)importedType).getMode()) &&
            ((CapabilityStatement.CapabilityStatementDocumentComponent)targetType).getProfile().equals(((CapabilityStatement.CapabilityStatementDocumentComponent)importedType).getProfile());
        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)
          match = ((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)importedType).getMode())
            && ((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)targetType).getDefinition().equals(((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)importedType).getDefinition());
        else if (importedType instanceof Extension) {
          if (((Extension)importedType).getUrl().equals(ExtensionConstants.EXT_CSDECLARED_PROFILE))
            match = ((Extension)targetType).getValueCanonicalType().getValue().equals(((Extension)importedType).getValueCanonicalType().getValue());
          else if (((Extension)importedType).getUrl().equals(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION)) {
            match = requiredSort(targetType).equals(requiredSort(importedType));
          } else
            throw new Error("Unexpected extension " + ((Extension)importedType).getUrl());
        } else
          throw new Error("Unhandled complex type in List match");
        if (match){
          foundType = targetType;
          break;
        }
      }
      if (foundType == null)
        targetList.add(importedType);
      else {
        if (importedType instanceof PrimitiveType) {
          // No properties to merge
        } else if (importedType instanceof CapabilityStatement.CapabilityStatementRestComponent)
          mergeProperties((CapabilityStatement.CapabilityStatementRestComponent)foundType, (CapabilityStatement.CapabilityStatementRestComponent)importedType, maxConformance, context);
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceComponent)
          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceComponent)importedType, maxConformance, context);
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceOperationComponent)
          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceOperationComponent)importedType, context);
        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)
          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)importedType, maxConformance, context);
        else if (importedType instanceof CapabilityStatement.ResourceInteractionComponent || importedType instanceof CapabilityStatement.SystemInteractionComponent) {
          // No properties to merge
        } else if (importedType instanceof CapabilityStatement.CapabilityStatementDocumentComponent) {
          // No properties to merge
        } else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingComponent)
          mergeProperties((CapabilityStatement.CapabilityStatementMessagingComponent)foundType, (CapabilityStatement.CapabilityStatementMessagingComponent)importedType, maxConformance, context);
        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent) {
          // No properties to merge
        } else if (importedType instanceof Extension) {
          if (((Extension) importedType).getUrl().equals(ExtensionConstants.EXT_CSDECLARED_PROFILE)) {
            // No action needed
          } else if (((Extension) importedType).getUrl().equals(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION))
            mergeSearchComboExt(((Extension) foundType), ((Extension) importedType), context + ".extension(SearchCombo - " + requiredSort(importedType) + ")");
        }
        mergeExpectations((Element) foundType, (Element) importedType, maxConformance);
      }
    }
  }

  /*
   * Two CodeableConcepts match if they have the same text and their codings match by code + system (and version if present)
   */
  private boolean match(CodeableConcept a, CodeableConcept b) {
    if (a.hasText() || b.hasText())
      if (a.hasText()!= b.hasText() || !a.getText().equals(b.getText()))
        return false;
    if (a.getCoding().size()!= b.getCoding().size())
      return false;
    for (Coding codeA: a.getCoding()) {
      boolean codingMatch = false;
      for (Coding codeB: b.getCoding()) {
        if (codeA.hasSystem() != codeB.hasSystem())
          continue;
        if (codeA.hasSystem() && !codeA.getSystem().equals(codeB.getSystem()))
          continue;
        if (codeA.hasCode() != codeB.hasCode())
          continue;
        if (codeA.hasCode() && !codeA.getCode().equals(codeB.getCode()))
          continue;
        if (codeA.hasVersion() != codeB.hasVersion())
          continue;
        if (codeA.hasVersion() && !codeA.getVersion().equals(codeB.getVersion()))
          continue;
        codingMatch = true;
        break;
      }
      if (!codingMatch)
        return false;
    }
    return true;
  }

  private List extensionValueList(Extension sortExtension, String url) {
    List aList = new ArrayList<>();
    for (Extension e: sortExtension.getExtensionsByUrl(url)) {
      aList.add(e.getValueStringType().toString());
    }
    aList.sort(new Utilities.CaseInsensitiveSorter());
    return aList;
  }

  private String requiredSort(Object sortExtension) {
    return String.join(";", extensionValueList((Extension)sortExtension, "required"));
  }

  private void mergeSearchComboExt(Extension targetExt, Extension importedExt, String context) {
    List targetList = extensionValueList(targetExt, "otional");
    List importedList = extensionValueList(importedExt, "otional");
    if (!targetList.containsAll(importedList))
      throw new FHIRException("Search Options extension for " + context + " does not contain all of the optional search names from the imported CapabilityStatement, which is not supported.");
  }

  private UnsignedIntType merge(UnsignedIntType targetInt, UnsignedIntType importedInt, String context) throws FHIRException {
    return merge(targetInt, importedInt, "SHALL", context);
  }

  private UnsignedIntType merge(UnsignedIntType targetInt, UnsignedIntType importedInt, String maxConformance, String context) throws FHIRException {
    if (targetInt == null)
      return importedInt;
    else if (importedInt == null)
      return (UnsignedIntType)fixMax(targetInt, context);
    else if (targetInt.getValue().equals(importedInt.getValue())) {
      mergeExpectations(targetInt, importedInt, maxConformance);
      return targetInt;
    } else if (targetInt.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedInt.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
      String targetExpectation = targetInt.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      String importedExpectation = importedInt.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      if (targetExpectation.equals(importedExpectation)) {
        if (targetExpectation.equals("SHALL"))
          throw new FHIRException("Non matching enumeration values with SHALL conformance expectations for " + context + " - base CapabilityStatement:" + targetInt.getValue() + "; imported CapabilityStatement:" + importedInt.getValue());
        else if (targetInt.getValue() > importedInt.getValue())
          return targetInt;
        else
          return (UnsignedIntType)fixMax(importedInt, maxConformance);
      } else {
        if (targetExpectation.equals("SHALL"))
          return targetInt;
        else if (importedExpectation.equals("SHALL"))
          return (UnsignedIntType)fixMax(importedInt, maxConformance);
        else if (targetInt.getValue() > importedInt.getValue())
          return targetInt;
        else
          return importedInt;
      }
    }
    throw new FHIRException("Non matching integer values for " + context + " - base CapabilityStatement:" + targetInt.getValue() + "; imported CapabilityStatement:" + importedInt.getValue());
  }


  private Enumeration merge(Enumeration targetCode, Enumeration importedCode, String context) throws FHIRException {
    return merge(targetCode, importedCode, "SHALL", context);
  }

  /*
   * Selects whichever code exists if only one exists, otherwise checks that the two codes match and merges conformance expectations
   */
  private Enumeration merge(Enumeration targetCode, Enumeration importedCode, String maxConformance, String context) throws FHIRException {
    if (targetCode == null || targetCode.getCode() == null)
      return (Enumeration)fixMax(importedCode, maxConformance);
    else if (importedCode == null || importedCode.getCode() == null)
      return targetCode;
    else if (targetCode.getValue().equals(importedCode.getValue())) {
      mergeExpectations(targetCode, importedCode, maxConformance);
      return targetCode;
    } else if (targetCode.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedCode.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
      String targetExpectation = targetCode.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      String importedExpectation = importedCode.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      int targetWeight = weightForEnum(targetCode, context);
      int importedWeight = weightForEnum(importedCode, context);
      if (targetExpectation.equals(importedExpectation)) {
        if (targetExpectation.equals("SHALL"))
          throw new FHIRException("Non matching enumeration values with SHALL conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
        else if (targetWeight == importedWeight)
          throw new FHIRException("Non matching enumeration values with equivalent weight and identical conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
        else if (targetWeight > importedWeight)
          return targetCode;
        else
          return (Enumeration)fixMax(importedCode, maxConformance);
      } else {
        if (targetExpectation.equals("SHALL"))
          return targetCode;
        else if (importedExpectation.equals("SHALL"))
          return (Enumeration)fixMax(importedCode, maxConformance);
        else if (targetWeight == importedWeight)
          throw new FHIRException("Non matching enumeration values with equivalent weight and optional conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
        else if (targetWeight > importedWeight)
          return targetCode;
        else
          return (Enumeration)fixMax(importedCode, maxConformance);
      }
    }
    throw new FHIRException("Non matching code values for " + context + " - base CapabilityStatement:" + targetCode.getCode() + "; imported CapabilityStatement:" + importedCode.getCode());
  }

  /*
   * Returns a numeric weight for enumeration codes that represent differing levels of sophistication.
   * Lower numbers imply lesser functionality that is implicitly included in higher numbers.  I.e. If you have a higher number it means you support the functionality of the lower numbers
   */
  private int weightForEnum(Enumeration code, String context) {
    switch (code.getSystem()) {
      case "http://hl7.org/fhir/conditional-delete-status":
        CapabilityStatement.ConditionalDeleteStatus deleteStatus = CapabilityStatement.ConditionalDeleteStatus.fromCode(code.getCode());
        switch (deleteStatus) {
          case NOTSUPPORTED:
            return 0;
          case SINGLE:
            return 1;
          case MULTIPLE:
            return 2;
          default:
            throw new FHIRException("Unrecognized Delete Status in " + context + ": " + code.getCode());
        }
      case "http://hl7.org/fhir/conditional-read-status":
        CapabilityStatement.ConditionalReadStatus readStatus = CapabilityStatement.ConditionalReadStatus.fromCode(code.getCode());
        switch (readStatus) {
          case NOTSUPPORTED:
            return 0;
          case MODIFIEDSINCE:
            return 1;
          case NOTMATCH:
            return 1; // Same weight as MODIFIEDSINCE
          case FULLSUPPORT:
            return 2;
          default:
            throw new FHIRException("Unrecognized Read Status in " + context + ": " + code.getCode());
        }
      case "http://hl7.org/fhir/versioning-policy":
        CapabilityStatement.ResourceVersionPolicy versionPolicy = CapabilityStatement.ResourceVersionPolicy.fromCode(code.getCode());
        switch (versionPolicy) {
          case NOVERSION:
            return 0;
          case VERSIONED:
            return 1;
          case VERSIONEDUPDATE:
            return 2;
          default:
            throw new FHIRException("Unrecognized Versioning Policy in " + context + ": " + code.getCode());
        }
    }
    throw new Error("Unsupported code system in " + context + ": " + code.getSystem());
  }

  protected BooleanType merge(BooleanType targetBool, BooleanType importedBool, String context) throws FHIRException {
    return merge(targetBool, importedBool, "SHALL", context);
  }

  /*
   * Selects whichever code exists if only one exists, otherwise checks that the two codes match and merges conformance expectations
   */
  protected BooleanType merge(BooleanType targetBool, BooleanType importedBool, String maxConformance, String context) throws FHIRException {
    if (targetBool == null || targetBool.getValue() == null)
      return (BooleanType)fixMax(importedBool,maxConformance);
    else if (importedBool == null || importedBool.getValue() == null)
      return targetBool;
    else if (targetBool.getValue().equals(importedBool.getValue())) {
      mergeExpectations(targetBool, importedBool, maxConformance);
      return targetBool;
    } else if (targetBool.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedBool.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
      String targetExpectation = targetBool.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      String importedExpectation = importedBool.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
      if (targetExpectation.equals(importedExpectation))
        throw new FHIRException("Non matching boolean values with equivalent conformance expectations for " + context + " - base CapabilityStatement:" + targetBool.getValue() + "; imported CapabilityStatement:" + importedBool.getValue());
      else if (targetExpectation.equals("SHALL"))
        return targetBool;
      else if (importedExpectation.equals("SHALL"))
        return (BooleanType)fixMax(importedBool, maxConformance);
      else if (targetExpectation.equals("SHOULD"))
        return targetBool;
      else if (importedExpectation.equals("SHOULD"))
        return (BooleanType)fixMax(importedBool, maxConformance);
    }
    throw new FHIRException("Non matching boolean values with no conformance expectations for " + context + " - base CapabilityStatement:" + targetBool.getValue() + "; imported CapabilityStatement:" + importedBool.getValue());
  }


  public void mergeExpectations(Element target, Element source, String maxConformance) {
    if (target.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
      Extension targetExpectation = target.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT);
      if (!targetExpectation.getValueCodeType().getCode().equals("SHALL") && source.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
        String sourceExpectation = effectiveConformance(source.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT), maxConformance);
        if (sourceExpectation.equals("SHALL") || targetExpectation.getValueCodeType().getCode().equals("MAY"))
          targetExpectation.setValue(new CodeType(sourceExpectation));
      }
    } else if (source.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
      target.addExtension(source.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT));
    }
  }

  private String effectiveConformance(String conf, String maxConf) {
    conf = conf==null ? "SHALL" : conf;
    maxConf = maxConf==null ? "SHALL" : maxConf;
    if (conf.equals(maxConf))
      return conf;
    else if (conf.equals("SHALL"))
      return maxConf;
    else if (maxConf.equals("SHALL") || maxConf.equals("SHOULD"))
      return conf;
    else
      return maxConf;
  }

  public DataType fixMax(DataType d, String maxConformance) {
    String conformance = d.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT);
    d.removeExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT);
    d.addExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT, new CodeType(effectiveConformance(conformance, maxConformance)));
    return d;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy