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