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}