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