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}