
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}