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}