001package ca.uhn.fhir.context.support; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 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.RuntimeResourceDefinition; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.util.BundleUtil; 029import org.apache.commons.lang3.StringUtils; 030import org.hl7.fhir.instance.model.api.IBase; 031import org.hl7.fhir.instance.model.api.IBaseBundle; 032import org.hl7.fhir.instance.model.api.IBaseResource; 033import org.hl7.fhir.instance.model.api.IPrimitiveType; 034 035import javax.annotation.Nullable; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.InputStreamReader; 039import java.util.ArrayList; 040import java.util.Collections; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Map; 044import java.util.Optional; 045import java.util.Properties; 046 047import static org.apache.commons.lang3.StringUtils.isNotBlank; 048 049/** 050 * This class returns the vocabulary that is shipped with the base FHIR 051 * specification. 052 * 053 * Note that this class is version aware. For example, a request for 054 * <code>http://foo-codesystem|123</code> will only return a value if 055 * the built in resource if the version matches. Unversioned URLs 056 * should generally be used, and will return whatever version is 057 * present. 058 */ 059public class DefaultProfileValidationSupport implements IValidationSupport { 060 061 private static final String URL_PREFIX_STRUCTURE_DEFINITION = "http://hl7.org/fhir/StructureDefinition/"; 062 private static final String URL_PREFIX_STRUCTURE_DEFINITION_BASE = "http://hl7.org/fhir/"; 063 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultProfileValidationSupport.class); 064 private final FhirContext myCtx; 065 066 private Map<String, IBaseResource> myCodeSystems; 067 private Map<String, IBaseResource> myStructureDefinitions; 068 private Map<String, IBaseResource> myValueSets; 069 private List<String> myTerminologyResources; 070 private List<String> myStructureDefinitionResources; 071 072 /** 073 * Constructor 074 * 075 * @param theFhirContext The context to use 076 */ 077 public DefaultProfileValidationSupport(FhirContext theFhirContext) { 078 myCtx = theFhirContext; 079 } 080 081 082 private void initializeResourceLists() { 083 084 if (myTerminologyResources != null && myStructureDefinitionResources != null) { 085 return; 086 } 087 088 List<String> terminologyResources = new ArrayList<>(); 089 List<String> structureDefinitionResources = new ArrayList<>(); 090 switch (getFhirContext().getVersion().getVersion()) { 091 case DSTU2: 092 case DSTU2_HL7ORG: 093 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/valuesets.xml"); 094 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/v2-tables.xml"); 095 terminologyResources.add("/org/hl7/fhir/instance/model/valueset/v3-codesystems.xml"); 096 Properties profileNameProperties = new Properties(); 097 try { 098 profileNameProperties.load(DefaultProfileValidationSupport.class.getResourceAsStream("/org/hl7/fhir/instance/model/profile/profiles.properties")); 099 for (Object nextKey : profileNameProperties.keySet()) { 100 structureDefinitionResources.add("/org/hl7/fhir/instance/model/profile/" + nextKey); 101 } 102 } catch (IOException e) { 103 throw new ConfigurationException(Msg.code(1740) + e); 104 } 105 break; 106 case DSTU2_1: 107 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/valuesets.xml"); 108 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/v2-tables.xml"); 109 terminologyResources.add("/org/hl7/fhir/dstu2016may/model/valueset/v3-codesystems.xml"); 110 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-resources.xml"); 111 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-types.xml"); 112 structureDefinitionResources.add("/org/hl7/fhir/dstu2016may/model/profile/profiles-others.xml"); 113 break; 114 case DSTU3: 115 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/valuesets.xml"); 116 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/v2-tables.xml"); 117 terminologyResources.add("/org/hl7/fhir/dstu3/model/valueset/v3-codesystems.xml"); 118 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-resources.xml"); 119 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-types.xml"); 120 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/profile/profiles-others.xml"); 121 structureDefinitionResources.add("/org/hl7/fhir/dstu3/model/extension/extension-definitions.xml"); 122 break; 123 case R4: 124 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/valuesets.xml"); 125 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/v2-tables.xml"); 126 terminologyResources.add("/org/hl7/fhir/r4/model/valueset/v3-codesystems.xml"); 127 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-resources.xml"); 128 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-types.xml"); 129 structureDefinitionResources.add("/org/hl7/fhir/r4/model/profile/profiles-others.xml"); 130 structureDefinitionResources.add("/org/hl7/fhir/r4/model/extension/extension-definitions.xml"); 131 break; 132 case R5: 133 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-resources.xml"); 134 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-types.xml"); 135 structureDefinitionResources.add("/org/hl7/fhir/r5/model/profile/profiles-others.xml"); 136 structureDefinitionResources.add("/org/hl7/fhir/r5/model/extension/extension-definitions.xml"); 137 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/valuesets.xml"); 138 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/v2-tables.xml"); 139 terminologyResources.add("/org/hl7/fhir/r5/model/valueset/v3-codesystems.xml"); 140 break; 141 } 142 143 myTerminologyResources = terminologyResources; 144 myStructureDefinitionResources = structureDefinitionResources; 145 } 146 147 148 @Override 149 public List<IBaseResource> fetchAllConformanceResources() { 150 ArrayList<IBaseResource> retVal = new ArrayList<>(); 151 retVal.addAll(myCodeSystems.values()); 152 retVal.addAll(myStructureDefinitions.values()); 153 retVal.addAll(myValueSets.values()); 154 return retVal; 155 } 156 157 @Override 158 public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { 159 return toList(provideStructureDefinitionMap()); 160 } 161 162 @Nullable 163 @Override 164 public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() { 165 return null; 166 } 167 168 169 @Override 170 public IBaseResource fetchCodeSystem(String theSystem) { 171 return fetchCodeSystemOrValueSet(theSystem, true); 172 } 173 174 private IBaseResource fetchCodeSystemOrValueSet(String theSystem, boolean codeSystem) { 175 synchronized (this) { 176 Map<String, IBaseResource> codeSystems = myCodeSystems; 177 Map<String, IBaseResource> valueSets = myValueSets; 178 if (codeSystems == null || valueSets == null) { 179 codeSystems = new HashMap<>(); 180 valueSets = new HashMap<>(); 181 182 initializeResourceLists(); 183 for (String next : myTerminologyResources) { 184 loadCodeSystems(codeSystems, valueSets, next); 185 } 186 187 myCodeSystems = codeSystems; 188 myValueSets = valueSets; 189 } 190 191 // System can take the form "http://url|version" 192 String system = theSystem; 193 String version = null; 194 int pipeIdx = system.indexOf('|'); 195 if (pipeIdx > 0) { 196 version = system.substring(pipeIdx + 1); 197 system = system.substring(0, pipeIdx); 198 } 199 200 IBaseResource candidate; 201 if (codeSystem) { 202 candidate = codeSystems.get(system); 203 } else { 204 candidate = valueSets.get(system); 205 } 206 207 if (candidate != null && isNotBlank(version) && !system.startsWith("http://hl7.org") && !system.startsWith("http://terminology.hl7.org")) { 208 if (!StringUtils.equals(version, myCtx.newTerser().getSinglePrimitiveValueOrNull(candidate, "version"))) { 209 candidate = null; 210 } 211 } 212 213 return candidate; 214 } 215 } 216 217 @Override 218 public IBaseResource fetchStructureDefinition(String theUrl) { 219 String url = theUrl; 220 if (url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) { 221 // no change 222 } else if (url.indexOf('/') == -1) { 223 url = URL_PREFIX_STRUCTURE_DEFINITION + url; 224 } else if (StringUtils.countMatches(url, '/') == 1) { 225 url = URL_PREFIX_STRUCTURE_DEFINITION_BASE + url; 226 } 227 Map<String, IBaseResource> structureDefinitionMap = provideStructureDefinitionMap(); 228 return structureDefinitionMap.get(url); 229 } 230 231 @Override 232 public IBaseResource fetchValueSet(String theUrl) { 233 IBaseResource retVal = fetchCodeSystemOrValueSet(theUrl, false); 234 return retVal; 235 } 236 237 public void flush() { 238 myCodeSystems = null; 239 myStructureDefinitions = null; 240 } 241 242 @Override 243 public FhirContext getFhirContext() { 244 return myCtx; 245 } 246 247 private Map<String, IBaseResource> provideStructureDefinitionMap() { 248 Map<String, IBaseResource> structureDefinitions = myStructureDefinitions; 249 if (structureDefinitions == null) { 250 structureDefinitions = new HashMap<>(); 251 252 initializeResourceLists(); 253 for (String next : myStructureDefinitionResources) { 254 loadStructureDefinitions(structureDefinitions, next); 255 } 256 257 myStructureDefinitions = structureDefinitions; 258 } 259 return structureDefinitions; 260 } 261 262 private void loadCodeSystems(Map<String, IBaseResource> theCodeSystems, Map<String, IBaseResource> theValueSets, String theClasspath) { 263 ourLog.info("Loading CodeSystem/ValueSet from classpath: {}", theClasspath); 264 InputStream inputStream = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath); 265 InputStreamReader reader = null; 266 if (inputStream != null) { 267 try { 268 reader = new InputStreamReader(inputStream, Constants.CHARSET_UTF8); 269 List<IBaseResource> resources = parseBundle(reader); 270 for (IBaseResource next : resources) { 271 272 RuntimeResourceDefinition nextDef = getFhirContext().getResourceDefinition(next); 273 Map<String, IBaseResource> map = null; 274 switch (nextDef.getName()) { 275 case "CodeSystem": 276 map = theCodeSystems; 277 break; 278 case "ValueSet": 279 map = theValueSets; 280 break; 281 } 282 283 if (map != null) { 284 String urlValueString = getConformanceResourceUrl(next); 285 if (isNotBlank(urlValueString)) { 286 map.put(urlValueString, next); 287 } 288 289 switch (myCtx.getVersion().getVersion()) { 290 case DSTU2: 291 case DSTU2_HL7ORG: 292 293 IPrimitiveType<?> codeSystem = myCtx.newTerser().getSingleValueOrNull(next, "ValueSet.codeSystem.system", IPrimitiveType.class); 294 if (codeSystem != null && isNotBlank(codeSystem.getValueAsString())) { 295 theCodeSystems.put(codeSystem.getValueAsString(), next); 296 } 297 298 break; 299 300 default: 301 case DSTU2_1: 302 case DSTU3: 303 case R4: 304 case R5: 305 break; 306 } 307 } 308 309 310 } 311 } finally { 312 try { 313 if (reader != null) { 314 reader.close(); 315 } 316 inputStream.close(); 317 } catch (IOException e) { 318 ourLog.warn("Failure closing stream", e); 319 } 320 } 321 } else { 322 ourLog.warn("Unable to load resource: {}", theClasspath); 323 } 324 } 325 326 private void loadStructureDefinitions(Map<String, IBaseResource> theCodeSystems, String theClasspath) { 327 ourLog.info("Loading structure definitions from classpath: {}", theClasspath); 328 try (InputStream valuesetText = DefaultProfileValidationSupport.class.getResourceAsStream(theClasspath)) { 329 if (valuesetText != null) { 330 try (InputStreamReader reader = new InputStreamReader(valuesetText, Constants.CHARSET_UTF8)) { 331 332 List<IBaseResource> resources = parseBundle(reader); 333 for (IBaseResource next : resources) { 334 335 String nextType = getFhirContext().getResourceType(next); 336 if ("StructureDefinition".equals(nextType)) { 337 338 String url = getConformanceResourceUrl(next); 339 if (isNotBlank(url)) { 340 theCodeSystems.put(url, next); 341 } 342 343 } 344 345 } 346 } 347 } else { 348 ourLog.warn("Unable to load resource: {}", theClasspath); 349 } 350 } catch (IOException theE) { 351 ourLog.warn("Unable to load resource: {}", theClasspath); 352 } 353 } 354 355 private String getConformanceResourceUrl(IBaseResource theResource) { 356 return getConformanceResourceUrl(getFhirContext(), theResource); 357 } 358 359 private List<IBaseResource> parseBundle(InputStreamReader theReader) { 360 IBaseResource parsedObject = getFhirContext().newXmlParser().parseResource(theReader); 361 if (parsedObject instanceof IBaseBundle) { 362 IBaseBundle bundle = (IBaseBundle) parsedObject; 363 return BundleUtil.toListOfResources(getFhirContext(), bundle); 364 } else { 365 return Collections.singletonList(parsedObject); 366 } 367 } 368 369 @Nullable 370 public static String getConformanceResourceUrl(FhirContext theFhirContext, IBaseResource theResource) { 371 String urlValueString = null; 372 Optional<IBase> urlValue = theFhirContext.getResourceDefinition(theResource).getChildByName("url").getAccessor().getFirstValueOrNull(theResource); 373 if (urlValue.isPresent()) { 374 IPrimitiveType<?> urlValueType = (IPrimitiveType<?>) urlValue.get(); 375 urlValueString = urlValueType.getValueAsString(); 376 } 377 return urlValueString; 378 } 379 380 static <T extends IBaseResource> List<T> toList(Map<String, IBaseResource> theMap) { 381 ArrayList<IBaseResource> retVal = new ArrayList<>(theMap.values()); 382 return (List<T>) Collections.unmodifiableList(retVal); 383 } 384}