
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}