001package org.hl7.fhir.r5.conformance;
002
003import org.hl7.fhir.exceptions.FHIRException;
004import org.hl7.fhir.r5.context.IWorkerContext;
005import org.hl7.fhir.r5.extensions.ExtensionConstants;
006import org.hl7.fhir.r5.model.*;
007import org.hl7.fhir.r5.model.Enumeration;
008import org.hl7.fhir.r5.utils.ToolingExtensions;
009import org.hl7.fhir.utilities.Utilities;
010
011import java.util.*;
012
013public class CapabilityStatementUtilities {
014  private IWorkerContext context;
015
016  public CapabilityStatementUtilities(IWorkerContext context) {
017    this.context = context;
018  }
019
020  /*
021   * Resolves any imported CapabilityStatements and returns a revised CapabilityStatement that merges all functionality from
022   * the imported CapabilityStatements
023   * @param targetCS - The CapabilityStatement (potentially) containing imports to be resolved
024   * @throws FHIRException - If there's an issue resolving any of the imports
025   */
026  public CapabilityStatement resolveImports(CapabilityStatement targetCS) throws FHIRException {
027    CapabilityStatement resolvedCS = targetCS.copy();
028    return resolveImports(resolvedCS, new HashMap<>(), "SHALL");
029  }
030
031  /*
032   * Resolves any imported CapabilityStatements and returns a revised CapabilityStatement that merges all functionality from
033   * the imported CapabilityStatements
034   * @param targetCS - The CapabilityStatement (potentially) containing imports to be resolved
035   * @param importedUrls - Keeps track of what CapabilityStatements have already been merged so that if the same CS appears more
036   *    than once in the hierarchy, we only process it once.  Also keeps track of what 'strength' the import is.
037   * @throws FHIRException - If there's an issue resolving any of the imports
038   *
039   * When processing imports, an imported CapabilityStatement can itself declare a conformance expectation.  If an import is a SHOULD or a MAY,
040   * then even if the imported CS asserts something as a SHALL, the highest effective level of conformance for the imported statements is the
041   * conformance level for the import itself.  And that cascades.  So if a MAY import points to a SHOULD import, the max conformance is 'MAY'.
042   *
043   * The merge process also tackles most of the semantically-significant extensions on CababilityStatement.
044   *
045   * Metadata is not merged - so things like description, publisher, etc. are taken only from the root importing CS.
046   */
047  public CapabilityStatement resolveImports(CapabilityStatement targetCS, Map<String, String> importedUrls, String conformance) throws FHIRException {
048    if (!targetCS.hasImports())
049      return targetCS;
050
051    CapabilityStatement resolvedCS = targetCS.copy();
052    for (CanonicalType canonical: resolvedCS.getImports()) {
053      CapabilityStatement importedCS = context.fetchResource(CapabilityStatement.class, canonical.getValue());
054      if (importedCS == null)
055        throw new FHIRException("Unable to resolve CapabilityStatement " + canonical.getValue() + " imported by " + targetCS.getUrl());
056      String importConformance = effectiveConformance(canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT), conformance);
057      if (importedUrls.containsKey(canonical.getValue()) && !importedUrls.get(canonical.getValue()).equals(importConformance)) {
058        throw new FHIRException("The CapabilityStatement " + canonical.getValue() + " is imported with different strengths - " + importedUrls.get(canonical.getValue()).equals(importConformance) + ", " + importConformance +
059          (importConformance.equals(canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT)) ? "" : "(effective from " + canonical.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT) + ")"));
060      }
061      importedUrls.put(targetCS.getUrl(), importConformance);
062
063      CapabilityStatement mergedImportedCS = resolveImports(importedCS, importedUrls, importConformance);
064      mergeCS(resolvedCS, mergedImportedCS, importConformance);
065    }
066
067    return resolvedCS;
068  }
069
070  /*
071   * Merges the details of an imported capability statement into the 'target' capability statement (which is a copy of the importing CS)
072   * The general import rules are as follows:
073   * - If the importing CS has something and the imported doesn't, grab what the importing does.
074   * - If the imported CS has something and the importing doesn't, grab what the imported does.
075   * - If both do something, combine the functionality and set the conformance expectation to the highest of the two
076   *
077   * Additional rules:
078   *  - CS allows you to specify separate messaging repetitions with different combinations of recipients.  That gets miserable to try to merge
079   *    because the potential recipients can be overlapping.  It's also super-rare to do that.  So this algorithm only handles merging if there's
080   *    a maximum of one messaging element (though that one element can list lots of supported messages)
081   *  - If there's non-repeating elements with different values, grab the one with the highest conformance expectation.  If the conformance levels
082   *    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,
083   *    then the SHALL takes precedence.  On the other hand, if both say SHOULD with different values, that's conflict and will trigger an exception.
084   *  - For certain 'coded' elements there's a hierarchy.  versioned-update implies versioned.  So if you have SHOULD:versioned and SHOULD:versioned-update,
085   *    that's *not* a conflict and you'll end up with SHOULD:versioned-update.  (Hierarchies are handled by the weightForEnum function.)
086   *  - 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
087   *  - For coded values, a match means all codings match (code, system, and version if present) and text matches.  Display names are ignored
088   *  - It's also a conflict if:
089   *    - the same operation 'code' is associated with different operation definitions
090   *    - the same search parameter 'code' is associated with different search parameter definitions
091   *    - 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
092   *      importing profile, but that was too hard to take on at this point)
093   *    - An imported search combination has more 'optional' elements than the importing search combination
094   *  - The following are additional limitations
095   *    - 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
096   *      a CS with messages that declare endpoints
097   *
098   *
099   */
100  protected void mergeCS(CapabilityStatement targetCS, CapabilityStatement importedCS, String maxConformance) {
101    merge(targetCS.getFormat(), importedCS.getFormat(), maxConformance, "format");
102    merge(targetCS.getPatchFormat(), importedCS.getPatchFormat(), maxConformance, "patchFormat");
103    merge(targetCS.getAcceptLanguage(), importedCS.getAcceptLanguage(), maxConformance, "acceptLanguage");
104    merge(targetCS.getImplementationGuide(), importedCS.getImplementationGuide(), maxConformance, "implementationGuide");
105    merge(targetCS.getRest(), importedCS.getRest(), maxConformance, "rest");
106    if (targetCS.getMessaging().size()>1)
107      throw new FHIRException("Unable to handle messaging repetitions greater than one for importing Capability Statement - use one repetition with multiple messaging.supportedMessage elements.");
108    else if (importedCS.getMessaging().size()>1)
109      throw new FHIRException("Unable to handle messaging repetitions greater than one for imported Capability Statement - use one repetition with multiple messaging.supportedMessage elements.");
110    else if (!importedCS.hasMessaging()) {
111      // Do nothing
112    } else if (!targetCS.hasMessaging())
113      targetCS.setMessaging(importedCS.getMessaging());
114    else {
115      CapabilityStatement.CapabilityStatementMessagingComponent targetMessaging = targetCS.getMessaging().get(0);
116      CapabilityStatement.CapabilityStatementMessagingComponent importedMessaging = importedCS.getMessaging().get(0);
117      merge(targetMessaging.getReliableCacheElement(), importedMessaging.getReliableCacheElement(), maxConformance, "messaging.reliableCache");
118      if (importedMessaging.hasEndpoint())
119        throw new FHIRException("Importing capability statements that assert endpoints is not supported");
120      merge(targetMessaging.getSupportedMessage(), importedMessaging.getSupportedMessage(), maxConformance, "messaging.supportedMessage");
121    }
122    merge(targetCS.getMessaging(), importedCS.getMessaging(), maxConformance, "messaging");
123    merge(targetCS.getDocument(), importedCS.getDocument(), maxConformance, "messaging");
124  }
125
126  void mergeProperties(CapabilityStatement.CapabilityStatementRestComponent targetType, CapabilityStatement.CapabilityStatementRestComponent importedType, String maxConformance, String context) {
127    String localContext = context + "." + targetType.getMode();
128
129    merge(targetType.getExtensionsByUrl(ExtensionConstants.EXT_CSDECLARED_PROFILE), importedType.getExtensionsByUrl(ExtensionConstants.EXT_CSDECLARED_PROFILE), maxConformance, ".extension(DeclaredProfile)");
130    merge(targetType.getExtensionsByUrl(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION), importedType.getExtensionsByUrl(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION), maxConformance, ".extension(SearchMode)");
131    if (!targetType.hasSecurity())
132      targetType.setSecurity(importedType.getSecurity());
133    else if (!importedType.hasSecurity())
134      return;
135    else {
136      mergeProperties(targetType.getSecurity(), importedType.getSecurity(), maxConformance, localContext);
137      mergeExpectations(targetType.getSecurity(), importedType.getSecurity(), maxConformance);
138    }
139    merge(targetType.getResource(), importedType.getResource(), maxConformance, localContext + ".resource");
140    merge(targetType.getInteraction(), importedType.getInteraction(), maxConformance, localContext + ".interaction");
141    merge(targetType.getOperation(), importedType.getOperation(), maxConformance, localContext + ".operation");
142    merge(targetType.getSearchParam(), importedType.getSearchParam(), maxConformance, localContext + ".searchParam");
143  }
144
145  /*
146   * Merges the properties of two RestSecurity components together
147   * NOTE: Doesn't merge documentation or extensions
148   */
149  // TODO: Handle known security extensions
150  void mergeProperties(CapabilityStatement.CapabilityStatementRestSecurityComponent targetType, CapabilityStatement.CapabilityStatementRestSecurityComponent importedType, String maxConformance, String context) {
151    merge(targetType.getCorsElement(), importedType.getCorsElement(), maxConformance, context + ".cors");
152    merge(targetType.getService(), importedType.getService(), maxConformance, context + ".service");
153  }
154
155  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceComponent targetType, CapabilityStatement.CapabilityStatementRestResourceComponent importedType, String maxConformance, String context) throws FHIRException {
156    String localContext = context + "." + targetType.getType();
157    if (targetType.hasProfile() && importedType.hasProfile() && !targetType.getProfile().equals(importedType.getProfile()))
158      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." +
159        "Importing: " + targetType.getProfile() + "; Imported: " + importedType.getProfile());
160    merge(targetType.getSupportedProfile(), importedType.getSupportedProfile(), maxConformance, localContext + ".supportedProfile");
161    targetType.setVersioningElement(merge(targetType.getVersioningElement(), importedType.getVersioningElement(), maxConformance, localContext + ".versioning"));
162    merge(targetType.getInteraction(), importedType.getInteraction(), maxConformance, localContext + ".interaction");
163    targetType.setReadHistoryElement(merge(targetType.getReadHistoryElement(), importedType.getReadHistoryElement(), maxConformance, localContext + ".readHistory"));
164    targetType.setUpdateCreateElement(merge(targetType.getUpdateCreateElement(), importedType.getUpdateCreateElement(), maxConformance, localContext + ".updateCreate"));
165    targetType.setConditionalCreateElement(merge(targetType.getConditionalCreateElement(), importedType.getConditionalCreateElement(), maxConformance, localContext + ".conditionalCreate"));
166    targetType.setConditionalReadElement(merge(targetType.getConditionalReadElement(), importedType.getConditionalReadElement(), maxConformance, localContext + ".conditionalRead"));
167    targetType.setConditionalPatchElement(merge(targetType.getConditionalPatchElement(), importedType.getConditionalPatchElement(), maxConformance, localContext + ".conditionalPatch"));
168    targetType.setConditionalDeleteElement(merge(targetType.getConditionalDeleteElement(), importedType.getConditionalDeleteElement(), maxConformance, localContext + ".conditionalDelete"));
169    merge(targetType.getReferencePolicy(), importedType.getReferencePolicy(), maxConformance, localContext + ".referencePolicy");
170    merge(targetType.getSearchInclude(), importedType.getSearchInclude(), maxConformance, localContext + ".searchInclude");
171    merge(targetType.getSearchRevInclude(), importedType.getSearchRevInclude(), maxConformance, localContext + ".searchRevInclude");
172    merge(targetType.getSearchParam(), importedType.getSearchParam(), maxConformance, localContext + ".searchParam");
173    merge(targetType.getOperation(), importedType.getOperation(), maxConformance, localContext + ".operation");
174  }
175
176  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceOperationComponent targetType, CapabilityStatement.CapabilityStatementRestResourceOperationComponent importedType, String context) throws FHIRException {
177    String localContext = context + "(name=" + targetType.getName() + ")";
178    if (!importedType.hasDefinition()) {
179      // do nothing
180    } else if (!targetType.hasDefinition())
181      targetType.setDefinitionElement(importedType.getDefinitionElement());
182    else if (!targetType.getDefinition().equals(importedType.getDefinition()))
183      throw new FHIRException("Differing definitions for same operation " + localContext + " in imported IG.  Importing:" + targetType.getDefinition() + "; imported:" + importedType.getDefinition());
184  }
185
186  void mergeProperties(CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent targetType, CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent importedType, String maxConformance, String context) throws FHIRException {
187    String localContext = context + "(name=" + targetType.getName() + ")";
188    if (!importedType.hasDefinition()) {
189      // do nothing
190    } else if (!targetType.hasDefinition()) {
191      targetType.setDefinitionElement((CanonicalType)fixMax(importedType.getDefinitionElement(), maxConformance));
192    } else if (!targetType.getDefinition().equals(importedType.getDefinition()))
193      throw new FHIRException("Differing definitions for same Search parameter " + localContext + " in imported IG.  Importing:" + targetType.getDefinition() + "; imported:" + importedType.getDefinition());
194    if (!importedType.hasType()) {
195      // do nothing
196    } else if (!targetType.hasType()) {
197      targetType.setTypeElement((Enumeration<Enumerations.SearchParamType>)fixMax(importedType.getTypeElement(), maxConformance));
198    } else if (!targetType.getType().equals(importedType.getType()))
199      throw new FHIRException("Differing search types for same Search parameter " + localContext + " in imported IG.  Importing:" + targetType.getType() + "; imported:" + importedType.getType());
200  }
201
202  void mergeProperties(CapabilityStatement.CapabilityStatementMessagingComponent targetType, CapabilityStatement.CapabilityStatementMessagingComponent importedType, String maxConformance, String context) throws FHIRException {
203    if (importedType.hasEndpoint()) {
204      throw new FHIRException("Cannot handle importing messaging with declared endpoints");
205    }
206    targetType.setReliableCacheElement(merge(targetType.getReliableCacheElement(), importedType.getReliableCacheElement(), maxConformance, context + ".reliableCache"));
207    merge(targetType.getSupportedMessage(), importedType.getSupportedMessage(), maxConformance, context + ".reliableCache");
208  }
209
210  void merge(List targetList, List importedList, String context) throws FHIRException {
211    merge(targetList, importedList, "SHALL", context);
212  }
213
214  /*
215   * Combines any 'simple' types found in the 'imported' list into the target list, merging conformance expectations found on matching codes
216   */
217  void merge(List targetList, List importedList, String maxConformance, String context) throws FHIRException {
218    for (Object importedType : importedList) {
219      Object foundType = null;
220      for (Object targetType : targetList) {
221        boolean match;
222        if (targetType instanceof PrimitiveType)
223          match = importedType.toString().equals(targetType.toString());
224        else if (importedType instanceof CodeableConcept)
225          match = match((CodeableConcept)targetType,(CodeableConcept)importedType);
226        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestComponent)
227          match = ((CapabilityStatement.CapabilityStatementRestComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementRestComponent)importedType).getMode());
228        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceComponent)
229          match = ((CapabilityStatement.CapabilityStatementRestResourceComponent)targetType).getType().equals(((CapabilityStatement.CapabilityStatementRestResourceComponent)importedType).getType());
230        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceOperationComponent)
231          match = ((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)targetType).getName().equals(((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)importedType).getName());
232        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)
233          match = ((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)targetType).getName().equals(((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)importedType).getName());
234        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingComponent)
235          match = true; // We only work if there's only one messaging component in each
236        else if (importedType instanceof CapabilityStatement.ResourceInteractionComponent)
237          match = ((CapabilityStatement.ResourceInteractionComponent)targetType).getCode().equals(((CapabilityStatement.ResourceInteractionComponent)importedType).getCode());
238        else if (importedType instanceof CapabilityStatement.SystemInteractionComponent)
239          match = ((CapabilityStatement.SystemInteractionComponent)targetType).getCode().equals(((CapabilityStatement.SystemInteractionComponent)importedType).getCode());
240        else if (importedType instanceof CapabilityStatement.CapabilityStatementDocumentComponent)
241          match = ((CapabilityStatement.CapabilityStatementDocumentComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementDocumentComponent)importedType).getMode()) &&
242            ((CapabilityStatement.CapabilityStatementDocumentComponent)targetType).getProfile().equals(((CapabilityStatement.CapabilityStatementDocumentComponent)importedType).getProfile());
243        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)
244          match = ((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)targetType).getMode().equals(((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)importedType).getMode())
245            && ((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)targetType).getDefinition().equals(((CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent)importedType).getDefinition());
246        else if (importedType instanceof Extension) {
247          if (((Extension)importedType).getUrl().equals(ExtensionConstants.EXT_CSDECLARED_PROFILE))
248            match = ((Extension)targetType).getValueCanonicalType().getValue().equals(((Extension)importedType).getValueCanonicalType().getValue());
249          else if (((Extension)importedType).getUrl().equals(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION)) {
250            match = requiredSort(targetType).equals(requiredSort(importedType));
251          } else
252            throw new Error("Unexpected extension " + ((Extension)importedType).getUrl());
253        } else
254          throw new Error("Unhandled complex type in List match");
255        if (match){
256          foundType = targetType;
257          break;
258        }
259      }
260      if (foundType == null)
261        targetList.add(importedType);
262      else {
263        if (importedType instanceof PrimitiveType) {
264          // No properties to merge
265        } else if (importedType instanceof CapabilityStatement.CapabilityStatementRestComponent)
266          mergeProperties((CapabilityStatement.CapabilityStatementRestComponent)foundType, (CapabilityStatement.CapabilityStatementRestComponent)importedType, maxConformance, context);
267        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceComponent)
268          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceComponent)importedType, maxConformance, context);
269        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceOperationComponent)
270          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceOperationComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceOperationComponent)importedType, context);
271        else if (importedType instanceof CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)
272          mergeProperties((CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)foundType, (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent)importedType, maxConformance, context);
273        else if (importedType instanceof CapabilityStatement.ResourceInteractionComponent || importedType instanceof CapabilityStatement.SystemInteractionComponent) {
274          // No properties to merge
275        } else if (importedType instanceof CapabilityStatement.CapabilityStatementDocumentComponent) {
276          // No properties to merge
277        } else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingComponent)
278          mergeProperties((CapabilityStatement.CapabilityStatementMessagingComponent)foundType, (CapabilityStatement.CapabilityStatementMessagingComponent)importedType, maxConformance, context);
279        else if (importedType instanceof CapabilityStatement.CapabilityStatementMessagingSupportedMessageComponent) {
280          // No properties to merge
281        } else if (importedType instanceof Extension) {
282          if (((Extension) importedType).getUrl().equals(ExtensionConstants.EXT_CSDECLARED_PROFILE)) {
283            // No action needed
284          } else if (((Extension) importedType).getUrl().equals(ExtensionConstants.EXT_CSSEARCH_PARAMETER_COMBINATION))
285            mergeSearchComboExt(((Extension) foundType), ((Extension) importedType), context + ".extension(SearchCombo - " + requiredSort(importedType) + ")");
286        }
287        mergeExpectations((Element) foundType, (Element) importedType, maxConformance);
288      }
289    }
290  }
291
292  /*
293   * Two CodeableConcepts match if they have the same text and their codings match by code + system (and version if present)
294   */
295  private boolean match(CodeableConcept a, CodeableConcept b) {
296    if (a.hasText() || b.hasText())
297      if (a.hasText()!= b.hasText() || !a.getText().equals(b.getText()))
298        return false;
299    if (a.getCoding().size()!= b.getCoding().size())
300      return false;
301    for (Coding codeA: a.getCoding()) {
302      boolean codingMatch = false;
303      for (Coding codeB: b.getCoding()) {
304        if (codeA.hasSystem() != codeB.hasSystem())
305          continue;
306        if (codeA.hasSystem() && !codeA.getSystem().equals(codeB.getSystem()))
307          continue;
308        if (codeA.hasCode() != codeB.hasCode())
309          continue;
310        if (codeA.hasCode() && !codeA.getCode().equals(codeB.getCode()))
311          continue;
312        if (codeA.hasVersion() != codeB.hasVersion())
313          continue;
314        if (codeA.hasVersion() && !codeA.getVersion().equals(codeB.getVersion()))
315          continue;
316        codingMatch = true;
317        break;
318      }
319      if (!codingMatch)
320        return false;
321    }
322    return true;
323  }
324
325  private List<String> extensionValueList(Extension sortExtension, String url) {
326    List<String> aList = new ArrayList<>();
327    for (Extension e: sortExtension.getExtensionsByUrl(url)) {
328      aList.add(e.getValueStringType().toString());
329    }
330    aList.sort(new Utilities.CaseInsensitiveSorter());
331    return aList;
332  }
333
334  private String requiredSort(Object sortExtension) {
335    return String.join(";", extensionValueList((Extension)sortExtension, "required"));
336  }
337
338  private void mergeSearchComboExt(Extension targetExt, Extension importedExt, String context) {
339    List<String> targetList = extensionValueList(targetExt, "otional");
340    List<String> importedList = extensionValueList(importedExt, "otional");
341    if (!targetList.containsAll(importedList))
342      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.");
343  }
344
345  private UnsignedIntType merge(UnsignedIntType targetInt, UnsignedIntType importedInt, String context) throws FHIRException {
346    return merge(targetInt, importedInt, "SHALL", context);
347  }
348
349  private UnsignedIntType merge(UnsignedIntType targetInt, UnsignedIntType importedInt, String maxConformance, String context) throws FHIRException {
350    if (targetInt == null)
351      return importedInt;
352    else if (importedInt == null)
353      return (UnsignedIntType)fixMax(targetInt, context);
354    else if (targetInt.getValue().equals(importedInt.getValue())) {
355      mergeExpectations(targetInt, importedInt, maxConformance);
356      return targetInt;
357    } else if (targetInt.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedInt.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
358      String targetExpectation = targetInt.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
359      String importedExpectation = importedInt.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
360      if (targetExpectation.equals(importedExpectation)) {
361        if (targetExpectation.equals("SHALL"))
362          throw new FHIRException("Non matching enumeration values with SHALL conformance expectations for " + context + " - base CapabilityStatement:" + targetInt.getValue() + "; imported CapabilityStatement:" + importedInt.getValue());
363        else if (targetInt.getValue() > importedInt.getValue())
364          return targetInt;
365        else
366          return (UnsignedIntType)fixMax(importedInt, maxConformance);
367      } else {
368        if (targetExpectation.equals("SHALL"))
369          return targetInt;
370        else if (importedExpectation.equals("SHALL"))
371          return (UnsignedIntType)fixMax(importedInt, maxConformance);
372        else if (targetInt.getValue() > importedInt.getValue())
373          return targetInt;
374        else
375          return importedInt;
376      }
377    }
378    throw new FHIRException("Non matching integer values for " + context + " - base CapabilityStatement:" + targetInt.getValue() + "; imported CapabilityStatement:" + importedInt.getValue());
379  }
380
381
382  private Enumeration merge(Enumeration targetCode, Enumeration importedCode, String context) throws FHIRException {
383    return merge(targetCode, importedCode, "SHALL", context);
384  }
385
386  /*
387   * Selects whichever code exists if only one exists, otherwise checks that the two codes match and merges conformance expectations
388   */
389  private Enumeration merge(Enumeration targetCode, Enumeration importedCode, String maxConformance, String context) throws FHIRException {
390    if (targetCode == null || targetCode.getCode() == null)
391      return (Enumeration)fixMax(importedCode, maxConformance);
392    else if (importedCode == null || importedCode.getCode() == null)
393      return targetCode;
394    else if (targetCode.getValue().equals(importedCode.getValue())) {
395      mergeExpectations(targetCode, importedCode, maxConformance);
396      return targetCode;
397    } else if (targetCode.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedCode.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
398      String targetExpectation = targetCode.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
399      String importedExpectation = importedCode.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
400      int targetWeight = weightForEnum(targetCode, context);
401      int importedWeight = weightForEnum(importedCode, context);
402      if (targetExpectation.equals(importedExpectation)) {
403        if (targetExpectation.equals("SHALL"))
404          throw new FHIRException("Non matching enumeration values with SHALL conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
405        else if (targetWeight == importedWeight)
406          throw new FHIRException("Non matching enumeration values with equivalent weight and identical conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
407        else if (targetWeight > importedWeight)
408          return targetCode;
409        else
410          return (Enumeration)fixMax(importedCode, maxConformance);
411      } else {
412        if (targetExpectation.equals("SHALL"))
413          return targetCode;
414        else if (importedExpectation.equals("SHALL"))
415          return (Enumeration)fixMax(importedCode, maxConformance);
416        else if (targetWeight == importedWeight)
417          throw new FHIRException("Non matching enumeration values with equivalent weight and optional conformance expectations for " + context + " - base CapabilityStatement:" + targetCode.getValue() + "; imported CapabilityStatement:" + importedCode.getValue());
418        else if (targetWeight > importedWeight)
419          return targetCode;
420        else
421          return (Enumeration)fixMax(importedCode, maxConformance);
422      }
423    }
424    throw new FHIRException("Non matching code values for " + context + " - base CapabilityStatement:" + targetCode.getCode() + "; imported CapabilityStatement:" + importedCode.getCode());
425  }
426
427  /*
428   * Returns a numeric weight for enumeration codes that represent differing levels of sophistication.
429   * 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
430   */
431  private int weightForEnum(Enumeration code, String context) {
432    switch (code.getSystem()) {
433      case "http://hl7.org/fhir/conditional-delete-status":
434        CapabilityStatement.ConditionalDeleteStatus deleteStatus = CapabilityStatement.ConditionalDeleteStatus.fromCode(code.getCode());
435        switch (deleteStatus) {
436          case NOTSUPPORTED:
437            return 0;
438          case SINGLE:
439            return 1;
440          case MULTIPLE:
441            return 2;
442          default:
443            throw new FHIRException("Unrecognized Delete Status in " + context + ": " + code.getCode());
444        }
445      case "http://hl7.org/fhir/conditional-read-status":
446        CapabilityStatement.ConditionalReadStatus readStatus = CapabilityStatement.ConditionalReadStatus.fromCode(code.getCode());
447        switch (readStatus) {
448          case NOTSUPPORTED:
449            return 0;
450          case MODIFIEDSINCE:
451            return 1;
452          case NOTMATCH:
453            return 1; // Same weight as MODIFIEDSINCE
454          case FULLSUPPORT:
455            return 2;
456          default:
457            throw new FHIRException("Unrecognized Read Status in " + context + ": " + code.getCode());
458        }
459      case "http://hl7.org/fhir/versioning-policy":
460        CapabilityStatement.ResourceVersionPolicy versionPolicy = CapabilityStatement.ResourceVersionPolicy.fromCode(code.getCode());
461        switch (versionPolicy) {
462          case NOVERSION:
463            return 0;
464          case VERSIONED:
465            return 1;
466          case VERSIONEDUPDATE:
467            return 2;
468          default:
469            throw new FHIRException("Unrecognized Versioning Policy in " + context + ": " + code.getCode());
470        }
471    }
472    throw new Error("Unsupported code system in " + context + ": " + code.getSystem());
473  }
474
475  protected BooleanType merge(BooleanType targetBool, BooleanType importedBool, String context) throws FHIRException {
476    return merge(targetBool, importedBool, "SHALL", context);
477  }
478
479  /*
480   * Selects whichever code exists if only one exists, otherwise checks that the two codes match and merges conformance expectations
481   */
482  protected BooleanType merge(BooleanType targetBool, BooleanType importedBool, String maxConformance, String context) throws FHIRException {
483    if (targetBool == null || targetBool.getValue() == null)
484      return (BooleanType)fixMax(importedBool,maxConformance);
485    else if (importedBool == null || importedBool.getValue() == null)
486      return targetBool;
487    else if (targetBool.getValue().equals(importedBool.getValue())) {
488      mergeExpectations(targetBool, importedBool, maxConformance);
489      return targetBool;
490    } else if (targetBool.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT) && importedBool.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
491      String targetExpectation = targetBool.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
492      String importedExpectation = importedBool.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT).getValueCodeType().getCode();
493      if (targetExpectation.equals(importedExpectation))
494        throw new FHIRException("Non matching boolean values with equivalent conformance expectations for " + context + " - base CapabilityStatement:" + targetBool.getValue() + "; imported CapabilityStatement:" + importedBool.getValue());
495      else if (targetExpectation.equals("SHALL"))
496        return targetBool;
497      else if (importedExpectation.equals("SHALL"))
498        return (BooleanType)fixMax(importedBool, maxConformance);
499      else if (targetExpectation.equals("SHOULD"))
500        return targetBool;
501      else if (importedExpectation.equals("SHOULD"))
502        return (BooleanType)fixMax(importedBool, maxConformance);
503    }
504    throw new FHIRException("Non matching boolean values with no conformance expectations for " + context + " - base CapabilityStatement:" + targetBool.getValue() + "; imported CapabilityStatement:" + importedBool.getValue());
505  }
506
507
508  public void mergeExpectations(Element target, Element source, String maxConformance) {
509    if (target.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
510      Extension targetExpectation = target.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT);
511      if (!targetExpectation.getValueCodeType().getCode().equals("SHALL") && source.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
512        String sourceExpectation = effectiveConformance(source.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT), maxConformance);
513        if (sourceExpectation.equals("SHALL") || targetExpectation.getValueCodeType().getCode().equals("MAY"))
514          targetExpectation.setValue(new CodeType(sourceExpectation));
515      }
516    } else if (source.hasExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT)) {
517      target.addExtension(source.getExtensionByUrl(ToolingExtensions.EXT_CAP_STMT_EXPECT));
518    }
519  }
520
521  private String effectiveConformance(String conf, String maxConf) {
522    conf = conf==null ? "SHALL" : conf;
523    maxConf = maxConf==null ? "SHALL" : maxConf;
524    if (conf.equals(maxConf))
525      return conf;
526    else if (conf.equals("SHALL"))
527      return maxConf;
528    else if (maxConf.equals("SHALL") || maxConf.equals("SHOULD"))
529      return conf;
530    else
531      return maxConf;
532  }
533
534  public DataType fixMax(DataType d, String maxConformance) {
535    String conformance = d.getExtensionString(ToolingExtensions.EXT_CAP_STMT_EXPECT);
536    d.removeExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT);
537    d.addExtension(ToolingExtensions.EXT_CAP_STMT_EXPECT, new CodeType(effectiveConformance(conformance, maxConformance)));
538    return d;
539  }
540}