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