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}