001package ca.uhn.fhir.context.support; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.ConfigurationException; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.context.FhirVersionEnum; 026import ca.uhn.fhir.context.RuntimeResourceDefinition; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.parser.LenientErrorHandler; 029import ca.uhn.fhir.rest.api.Constants; 030import ca.uhn.fhir.util.BundleUtil; 031import ca.uhn.fhir.util.ClasspathUtil; 032import org.apache.commons.lang3.StringUtils; 033import org.hl7.fhir.instance.model.api.IBase; 034import org.hl7.fhir.instance.model.api.IBaseBundle; 035import org.hl7.fhir.instance.model.api.IBaseResource; 036import org.hl7.fhir.instance.model.api.IPrimitiveType; 037 038import javax.annotation.Nullable; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.InputStreamReader; 042import java.util.ArrayList; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.List; 046import java.util.Locale; 047import java.util.Map; 048import java.util.Optional; 049import java.util.Properties; 050 051import static org.apache.commons.lang3.StringUtils.isNotBlank; 052 053/** 054 * This class returns the vocabulary that is shipped with the base FHIR 055 * specification. 056 * 057 * Note that this class is version aware. For example, a request for 058 * <code>http://foo-codesystem|123</code> will only return a value if 059 * the built in resource if the version matches. Unversioned URLs 060 * should generally be used, and will return whatever version is 061 * present. 062 */ 063public class DefaultProfileValidationSupport implements IValidationSupport { 064 065 private static final String URL_PREFIX_STRUCTURE_DEFINITION = "http://hl7.org/fhir/StructureDefinition/"; 066 private static final String URL_PREFIX_STRUCTURE_DEFINITION_BASE = "http://hl7.org/fhir/"; 067 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultProfileValidationSupport.class); 068 private final FhirContext myCtx; 069 070 private Map<String, IBaseResource> myCodeSystems; 071 private Map<String, IBaseResource> myStructureDefinitions; 072 private Map<String, IBaseResource> myValueSets; 073 private List<String> myTerminologyResources; 074 private List<String> myStructureDefinitionResources; 075 076 /** 077 * Constructor 078 * 079 * @param theFhirContext The context to use 080 */ 081 public DefaultProfileValidationSupport(FhirContext theFhirContext) { 082 myCtx = theFhirContext; 083 } 084 085 086 private void initializeResourceLists() { 087 088 if (myTerminologyResources != null && myStructureDefinitionResources != null) { 089 return; 090 } 091 092 List<String> terminologyResources = new ArrayList<>(); 093 List<String> structureDefinitionResources = new ArrayList<>(); 094 switch (getFhirContext().getVersion().getVersion()) { 095 case DSTU2: 096 case DSTU2_HL7ORG: 097 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/valuesets.xml"); 098 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/v2-tables.xml"); 099 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/v3-codesystems.xml"); 100 Properties profileNameProperties = new Properties(); 101 try { 102 profileNameProperties.load(DefaultProfileValidationSupport.class.getResourceAsStream("/org/hl7/fhir/instance/model/profile/profiles.properties")); 103 for (Object nextKey : profileNameProperties.keySet()) { 104 structureDefinitionResources.add("/org/hl7/fhir/instance/model/profile/" + nextKey); 105 } 106 } catch (IOException e) { 107 throw new ConfigurationException(Msg.code(1740) + e); 108 } 109 break; 110 case DSTU2_1: 111 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/valuesets.xml"); 112 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/v2-tables.xml"); 113 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/v3-codesystems.xml"); 114 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-resources.xml"); 115 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-types.xml"); 116 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-others.xml"); 117 break; 118 case DSTU3: 119 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/valuesets.xml"); 120 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/v2-tables.xml"); 121 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/v3-codesystems.xml"); 122 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-resources.xml"); 123 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-types.xml"); 124 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-others.xml"); 125 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/extension/extension-definitions.xml"); 126 break; 127 case R4: 128 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/valuesets.xml"); 129 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/v2-tables.xml"); 130 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/v3-codesystems.xml"); 131 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-resources.xml"); 132 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-types.xml"); 133 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-others.xml"); 134 structureDefinitionResources.add("/org/hl7/fhir/r4/model/extension/extension-definitions.xml"); 135 break; 136 case R4B: 137 terminologyResources.add("/org/hl7/fhir/r4b/model/valueset/valuesets.xml"); 138 terminologyResources.add("/org/hl7/fhir/r4b/model/valueset/v2-tables.xml"); 139 terminologyResources.add("/org/hl7/fhir/r4b/model/valueset/v3-codesystems.xml"); 140 structureDefinitionResources.add("/org/hl7/fhir/r4b/model/profile/profiles-resources.xml"); 141 structureDefinitionResources.add("/org/hl7/fhir/r4b/model/profile/profiles-types.xml"); 142 structureDefinitionResources.add("/org/hl7/fhir/r4b/model/profile/profiles-others.xml"); 143 structureDefinitionResources.add("/org/hl7/fhir/r4b/model/extension/extension-definitions.xml"); 144 break; 145 case R5: 146 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-resources.xml"); 147 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-types.xml"); 148 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-others.xml"); 149 structureDefinitionResources.add("/org/hl7/fhir/r5/model/extension/extension-definitions.xml"); 150 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/valuesets.xml"); 151 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/v2-tables.xml"); 152 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/v3-codesystems.xml"); 153 break; 154 } 155 156 myTerminologyResources = terminologyResources; 157 myStructureDefinitionResources = structureDefinitionResources; 158 } 159 160 161 @Override 162 public List<IBaseResource> fetchAllConformanceResources() { 163 ArrayList<IBaseResource> retVal = new ArrayList<>(); 164 retVal.addAll(myCodeSystems.values()); 165 retVal.addAll(myStructureDefinitions.values()); 166 retVal.addAll(myValueSets.values()); 167 return retVal; 168 } 169 170 @Override 171 public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { 172 return toList(provideStructureDefinitionMap()); 173 } 174 175 @Nullable 176 @Override 177 public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() { 178 return null; 179 } 180 181 182 @Override 183 public IBaseResource fetchCodeSystem(String theSystem) { 184 return fetchCodeSystemOrValueSet(theSystem, true); 185 } 186 187 private IBaseResource fetchCodeSystemOrValueSet(String theSystem, boolean codeSystem) { 188 synchronized (this) { 189 Map<String, IBaseResource> codeSystems = myCodeSystems; 190 Map<String, IBaseResource> valueSets = myValueSets; 191 if (codeSystems == null || valueSets == null) { 192 codeSystems = new HashMap<>(); 193 valueSets = new HashMap<>(); 194 195 initializeResourceLists(); 196 for (String next : myTerminologyResources) { 197 loadCodeSystems(codeSystems, valueSets, next); 198 } 199 200 myCodeSystems = codeSystems; 201 myValueSets = valueSets; 202 } 203 204 // System can take the form "http://url|version" 205 String system = theSystem; 206 String version = null; 207 int pipeIdx = system.indexOf('|'); 208 if (pipeIdx > 0) { 209 version = system.substring(pipeIdx + 1); 210 system = system.substring(0, pipeIdx); 211 } 212 213 IBaseResource candidate; 214 if (codeSystem) { 215 candidate = codeSystems.get(system); 216 } else { 217 candidate = valueSets.get(system); 218 } 219 220 if (candidate != null && isNotBlank(version) && !system.startsWith("http://hl7.org") && !system.startsWith("http://terminology.hl7.org")) { 221 if (!StringUtils.equals(version, myCtx.newTerser().getSinglePrimitiveValueOrNull(candidate, "version"))) { 222 candidate = null; 223 } 224 } 225 226 return candidate; 227 } 228 } 229 230 @Override 231 public IBaseResource fetchStructureDefinition(String theUrl) { 232 String url = theUrl; 233 if (!url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) { 234 if (url.indexOf('/') == -1) { 235 url = URL_PREFIX_STRUCTURE_DEFINITION + url; 236 } else if (StringUtils.countMatches(url, '/') == 1) { 237 url = URL_PREFIX_STRUCTURE_DEFINITION_BASE + url; 238 } 239 } 240 Map<String, IBaseResource> structureDefinitionMap = provideStructureDefinitionMap(); 241 IBaseResource retVal = structureDefinitionMap.get(url); 242 if (retVal == null) { 243 244 if (url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) { 245 246 /* 247 * A few built-in R4 SearchParameters have the wrong casing for primitive 248 * search parameters eg "value.as(String)" when it should be 249 * "value.as(string)". This lets us be a bit lenient about this. 250 */ 251 if (myCtx.getVersion().getVersion() == FhirVersionEnum.R4 || myCtx.getVersion().getVersion() == FhirVersionEnum.R4B || myCtx.getVersion().getVersion() == FhirVersionEnum.R5) { 252 String end = url.substring(URL_PREFIX_STRUCTURE_DEFINITION.length()); 253 if (Character.isUpperCase(end.charAt(0))) { 254 String newEnd = Character.toLowerCase(end.charAt(0)) + end.substring(1); 255 String alternateUrl = URL_PREFIX_STRUCTURE_DEFINITION + newEnd; 256 retVal = structureDefinitionMap.get(alternateUrl); 257 if (retVal != null) { 258 retVal = myCtx.newTerser().clone(retVal); 259 myCtx.newTerser().setElement(retVal, "type", end); 260 } 261 } 262 } 263 264 } 265 266 } 267 return retVal; 268 } 269 270 @Override 271 public IBaseResource fetchValueSet(String theUrl) { 272 IBaseResource retVal = fetchCodeSystemOrValueSet(theUrl, false); 273 return retVal; 274 } 275 276 public void flush() { 277 myCodeSystems = null; 278 myStructureDefinitions = null; 279 } 280 281 @Override 282 public FhirContext getFhirContext() { 283 return myCtx; 284 } 285 286 private Map<String, IBaseResource> provideStructureDefinitionMap() { 287 Map<String, IBaseResource> structureDefinitions = myStructureDefinitions; 288 if (structureDefinitions == null) { 289 structureDefinitions = new HashMap<>(); 290 291 initializeResourceLists(); 292 for (String next : myStructureDefinitionResources) { 293 loadStructureDefinitions(structureDefinitions, next); 294 } 295 296 myStructureDefinitions = structureDefinitions; 297 } 298 return structureDefinitions; 299 } 300 301 private void loadCodeSystems(Map<String, IBaseResource> theCodeSystems, Map<String, IBaseResource> theValueSets, String theClasspath) { 302 ourLog.info("Loading CodeSystem/ValueSet from classpath: {}", theClasspath); 303 InputStream inputStream = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath); 304 InputStreamReader reader = null; 305 if (inputStream != null) { 306 try { 307 reader = new InputStreamReader(inputStream, Constants.CHARSET_UTF8); 308 List<IBaseResource> resources = parseBundle(reader); 309 for (IBaseResource next : resources) { 310 311 RuntimeResourceDefinition nextDef = getFhirContext().getResourceDefinition(next); 312 Map<String, IBaseResource> map = null; 313 switch (nextDef.getName()) { 314 case "CodeSystem": 315 map = theCodeSystems; 316 break; 317 case "ValueSet": 318 map = theValueSets; 319 break; 320 } 321 322 if (map != null) { 323 String urlValueString = getConformanceResourceUrl(next); 324 if (isNotBlank(urlValueString)) { 325 map.put(urlValueString, next); 326 } 327 328 switch (myCtx.getVersion().getVersion()) { 329 case DSTU2: 330 case DSTU2_HL7ORG: 331 332 IPrimitiveType<?> codeSystem = myCtx.newTerser().getSingleValueOrNull(next, "ValueSet.codeSystem.system", IPrimitiveType.class); 333 if (codeSystem != null && isNotBlank(codeSystem.getValueAsString())) { 334 theCodeSystems.put(codeSystem.getValueAsString(), next); 335 } 336 337 break; 338 339 default: 340 case DSTU2_1: 341 case DSTU3: 342 case R4: 343 case R5: 344 break; 345 } 346 } 347 348 349 } 350 } finally { 351 try { 352 if (reader != null) { 353 reader.close(); 354 } 355 inputStream.close(); 356 } catch (IOException e) { 357 ourLog.warn("Failure closing stream", e); 358 } 359 } 360 } else { 361 ourLog.warn("Unable to load resource: {}", theClasspath); 362 } 363 364 // Load built-in system 365 366 if (myCtx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 367 String storageCodeEnum = ClasspathUtil.loadResource("ca/uhn/fhir/context/support/HapiFhirStorageResponseCode.json"); 368 IBaseResource storageCodeCodeSystem = myCtx.newJsonParser().setParserErrorHandler(new LenientErrorHandler()).parseResource(storageCodeEnum); 369 String url = myCtx.newTerser().getSinglePrimitiveValueOrNull(storageCodeCodeSystem, "url"); 370 theCodeSystems.put(url, storageCodeCodeSystem); 371 } 372 373 374 375 376 } 377 378 private void loadStructureDefinitions(Map<String, IBaseResource> theCodeSystems, String theClasspath) { 379 ourLog.info("Loading structure definitions from classpath: {}", theClasspath); 380 381 String packageUserData = null; 382 if (myCtx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 383 packageUserData = "hl7.fhir." + myCtx.getVersion().getVersion().name().replace("DSTU", "R").toLowerCase(Locale.US); 384 } 385 386 try (InputStream valueSetText = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath)) { 387 if (valueSetText != null) { 388 try (InputStreamReader reader = new InputStreamReader(valueSetText, Constants.CHARSET_UTF8)) { 389 390 List<IBaseResource> resources = parseBundle(reader); 391 for (IBaseResource next : resources) { 392 393 String nextType = getFhirContext().getResourceType(next); 394 if ("StructureDefinition".equals(nextType)) { 395 396 String url = getConformanceResourceUrl(next); 397 if (isNotBlank(url)) { 398 theCodeSystems.put(url, next); 399 } 400 401 } 402 403 // This is used by the validator to determine which package a given SD came from. 404 // I don't love this use of magic strings but that's what is expected currently 405 if (packageUserData != null) { 406 next.setUserData("package", packageUserData); 407 } 408 409 } 410 } 411 } else { 412 ourLog.warn("Unable to load resource: {}", theClasspath); 413 } 414 } catch (IOException theE) { 415 ourLog.warn("Unable to load resource: {}", theClasspath); 416 } 417 } 418 419 private String getConformanceResourceUrl(IBaseResource theResource) { 420 return getConformanceResourceUrl(getFhirContext(), theResource); 421 } 422 423 private List<IBaseResource> parseBundle(InputStreamReader theReader) { 424 IBaseResource parsedObject = getFhirContext().newXmlParser().parseResource(theReader); 425 if (parsedObject instanceof IBaseBundle) { 426 IBaseBundle bundle = (IBaseBundle) parsedObject; 427 return BundleUtil.toListOfResources(getFhirContext(), bundle); 428 } else { 429 return Collections.singletonList(parsedObject); 430 } 431 } 432 433 @Nullable 434 public static String getConformanceResourceUrl(FhirContext theFhirContext, IBaseResource theResource) { 435 String urlValueString = null; 436 Optional<IBase> urlValue = theFhirContext.getResourceDefinition(theResource).getChildByName("url").getAccessor().getFirstValueOrNull(theResource); 437 if (urlValue.isPresent()) { 438 IPrimitiveType<?> urlValueType = (IPrimitiveType<?>) urlValue.get(); 439 urlValueString = urlValueType.getValueAsString(); 440 } 441 return urlValueString; 442 } 443 444 static <T extends IBaseResource> List<T> toList(Map<String, IBaseResource> theMap) { 445 ArrayList<IBaseResource> retVal = new ArrayList<>(theMap.values()); 446 return (List<T>) Collections.unmodifiableList(retVal); 447 } 448}