001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.FhirVersionEnum;
024import ca.uhn.fhir.context.support.IValidationSupport;
025import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
026import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
027import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
028import ca.uhn.fhir.jpa.term.TermReadSvcUtil;
029import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
030import ca.uhn.fhir.model.primitive.IdDt;
031import ca.uhn.fhir.rest.api.SortOrderEnum;
032import ca.uhn.fhir.rest.api.SortSpec;
033import ca.uhn.fhir.rest.api.server.IBundleProvider;
034import ca.uhn.fhir.rest.param.StringParam;
035import ca.uhn.fhir.rest.param.TokenParam;
036import ca.uhn.fhir.rest.param.UriParam;
037import ca.uhn.fhir.sl.cache.Cache;
038import ca.uhn.fhir.sl.cache.CacheFactory;
039import org.apache.commons.lang3.Validate;
040import org.hl7.fhir.instance.model.api.IAnyResource;
041import org.hl7.fhir.instance.model.api.IBaseResource;
042import org.hl7.fhir.r4.model.CodeSystem;
043import org.hl7.fhir.r4.model.IdType;
044import org.hl7.fhir.r4.model.ImplementationGuide;
045import org.hl7.fhir.r4.model.Questionnaire;
046import org.hl7.fhir.r4.model.StructureDefinition;
047import org.hl7.fhir.r4.model.UriType;
048import org.hl7.fhir.r4.model.ValueSet;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051import org.springframework.beans.factory.annotation.Autowired;
052import org.springframework.transaction.annotation.Propagation;
053import org.springframework.transaction.annotation.Transactional;
054
055import javax.annotation.Nullable;
056import javax.annotation.PostConstruct;
057import java.util.Arrays;
058import java.util.List;
059import java.util.Optional;
060import java.util.concurrent.TimeUnit;
061import java.util.function.Supplier;
062
063import static org.apache.commons.lang3.StringUtils.isBlank;
064import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW;
065
066/**
067 * This class is a {@link IValidationSupport Validation support} module that loads
068 * validation resources (StructureDefinition, ValueSet, CodeSystem, etc.) from the resources
069 * persisted in the JPA server.
070 */
071@Transactional(propagation = Propagation.REQUIRED)
072public class JpaPersistedResourceValidationSupport implements IValidationSupport {
073
074        private static final Logger ourLog = LoggerFactory.getLogger(JpaPersistedResourceValidationSupport.class);
075
076        private final FhirContext myFhirContext;
077        private final IBaseResource myNoMatch;
078
079        @Autowired
080        private DaoRegistry myDaoRegistry;
081
082        @Autowired
083        private ITermReadSvc myTermReadSvc;
084
085        private Class<? extends IBaseResource> myCodeSystemType;
086        private Class<? extends IBaseResource> myStructureDefinitionType;
087        private Class<? extends IBaseResource> myValueSetType;
088
089        // TODO: JA2 We shouldn't need to cache here, but we probably still should since the
090        // TermReadSvcImpl calls these methods as a part of its "isCodeSystemSupported" calls.
091        // We should modify CachingValidationSupport to cache the results of "isXXXSupported"
092        // at which point we could do away with this cache
093    private Cache<String, IBaseResource> myLoadCache = CacheFactory.build(TimeUnit.MINUTES.toMillis(1), 1000);
094
095        /**
096         * Constructor
097         */
098        public JpaPersistedResourceValidationSupport(FhirContext theFhirContext) {
099                super();
100                Validate.notNull(theFhirContext);
101                myFhirContext = theFhirContext;
102
103                myNoMatch = myFhirContext.getResourceDefinition("Basic").newInstance();
104        }
105
106
107        @Override
108        public IBaseResource fetchCodeSystem(String theSystem) {
109                if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) {
110                        Optional<IBaseResource> currentCSOpt = getCodeSystemCurrentVersion(new UriType(theSystem));
111                        if (!currentCSOpt.isPresent()) {
112                                ourLog.info("Couldn't find current version of CodeSystem: " + theSystem);
113                        }
114                        return currentCSOpt.orElse(null);
115                }
116
117                return fetchResource(myCodeSystemType, theSystem);
118        }
119
120        /**
121         * Obtains the current version of a CodeSystem using the fact that the current
122         * version is always pointed by the ForcedId for the no-versioned CS
123         */
124        private Optional<IBaseResource> getCodeSystemCurrentVersion(UriType theUrl) {
125        if (!theUrl.getValueAsString().contains(LOINC_LOW)) {
126           return Optional.empty();
127        }
128
129                return myTermReadSvc.readCodeSystemByForcedId(LOINC_LOW);
130        }
131
132
133        @Override
134        public IBaseResource fetchValueSet(String theSystem) {
135                if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) {
136                        Optional<IBaseResource> currentVSOpt = getValueSetCurrentVersion(new UriType(theSystem));
137                        return currentVSOpt.orElse(null);
138                }
139
140                return fetchResource(myValueSetType, theSystem);
141        }
142
143        /**
144         * Obtains the current version of a ValueSet using the fact that the current
145         * version is always pointed by the ForcedId for the no-versioned VS
146         */
147        private Optional<IBaseResource> getValueSetCurrentVersion(UriType theUrl) {
148                Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl.getValueAsString());
149        if (!vsIdOpt.isPresent()) {
150            return Optional.empty();
151        }
152
153                IFhirResourceDao<? extends IBaseResource> valueSetResourceDao = myDaoRegistry.getResourceDao(myValueSetType);
154                IBaseResource valueSet = valueSetResourceDao.read(new IdDt("ValueSet", vsIdOpt.get()));
155                return Optional.ofNullable(valueSet);
156        }
157
158
159        @Override
160        public IBaseResource fetchStructureDefinition(String theUrl) {
161                return fetchResource(myStructureDefinitionType, theUrl);
162        }
163
164        @SuppressWarnings("unchecked")
165        @Nullable
166        @Override
167        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
168                if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) {
169                        return null;
170                }
171                IBundleProvider search = myDaoRegistry.getResourceDao("StructureDefinition").search(new SearchParameterMap().setLoadSynchronousUpTo(1000));
172                return (List<T>) search.getResources(0, 1000);
173        }
174
175        @Override
176        @SuppressWarnings({"unchecked", "unused"})
177        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
178                if (isBlank(theUri)) {
179                        return null;
180                }
181
182                String key = theClass + " " + theUri;
183                IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri));
184
185                if (fetched == myNoMatch) {
186                        return null;
187                }
188
189                return (T) fetched;
190        }
191
192        private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) {
193                if (theClass == null) {
194                        Supplier<IBaseResource>[] fetchers = new Supplier[]{
195                                () -> doFetchResource(ValueSet.class, theUri),
196                                () -> doFetchResource(CodeSystem.class, theUri),
197                                () -> doFetchResource(StructureDefinition.class, theUri)
198                        };
199                        return Arrays
200                                .stream(fetchers)
201                                .map(t -> t.get())
202                                .filter(t -> t != myNoMatch)
203                                .findFirst()
204                                .orElse(myNoMatch);
205                }
206
207                IdType id = new IdType(theUri);
208                boolean localReference = false;
209                if (id.hasBaseUrl() == false && id.hasIdPart() == true) {
210                        localReference = true;
211                }
212
213                String resourceName = myFhirContext.getResourceType(theClass);
214                IBundleProvider search;
215                switch (resourceName) {
216                        case "ValueSet":
217                                if (localReference) {
218                                        SearchParameterMap params = new SearchParameterMap();
219                                        params.setLoadSynchronousUpTo(1);
220                                        params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
221                                        search = myDaoRegistry.getResourceDao(resourceName).search(params);
222                                        if (search.size() == 0) {
223                                                params = new SearchParameterMap();
224                                                params.setLoadSynchronousUpTo(1);
225                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
226                                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
227                                        }
228                                } else {
229                                        int versionSeparator = theUri.lastIndexOf('|');
230                                        SearchParameterMap params = new SearchParameterMap();
231                                        params.setLoadSynchronousUpTo(1);
232                                        if (versionSeparator != -1) {
233                                                params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
234                                                params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
235                                        } else {
236                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
237                                        }
238                                        params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
239                                        search = myDaoRegistry.getResourceDao(resourceName).search(params);
240
241                                        if (search.isEmpty() && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
242                                                params = new SearchParameterMap();
243                                                params.setLoadSynchronousUpTo(1);
244                                                if (versionSeparator != -1) {
245                                                        params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
246                                                        params.add("system", new UriParam(theUri.substring(0, versionSeparator)));
247                                                } else {
248                                                        params.add("system", new UriParam(theUri));
249                                                }
250                                                params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
251                                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
252                                        }
253
254                                }
255                                break;
256                        case "StructureDefinition": {
257                                // Don't allow the core FHIR definitions to be overwritten
258                                if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
259                                        String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
260                                        if (myFhirContext.getElementDefinition(typeName) != null) {
261                                                return myNoMatch;
262                                        }
263                                }
264                                SearchParameterMap params = new SearchParameterMap();
265                                params.setLoadSynchronousUpTo(1);
266                                params.add(StructureDefinition.SP_URL, new UriParam(theUri));
267                                search = myDaoRegistry.getResourceDao("StructureDefinition").search(params);
268                                break;
269                        }
270                        case "Questionnaire": {
271                                SearchParameterMap params = new SearchParameterMap();
272                                params.setLoadSynchronousUpTo(1);
273                                if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
274                                        params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
275                                } else {
276                                        params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
277                                }
278                                search = myDaoRegistry.getResourceDao("Questionnaire").search(params);
279                                break;
280                        }
281                        case "CodeSystem": {
282                                int versionSeparator = theUri.lastIndexOf('|');
283                                SearchParameterMap params = new SearchParameterMap();
284                                params.setLoadSynchronousUpTo(1);
285                                if (versionSeparator != -1) {
286                                        params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
287                                        params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
288                                } else {
289                                        params.add(CodeSystem.SP_URL, new UriParam(theUri));
290                                }
291                                params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
292                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
293                                break;
294                        }
295                        case "ImplementationGuide":
296                        case "SearchParameter": {
297                                SearchParameterMap params = new SearchParameterMap();
298                                params.setLoadSynchronousUpTo(1);
299                                params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
300                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
301                                break;
302                        }
303                        default:
304                                // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question has a URL
305                                SearchParameterMap params = new SearchParameterMap();
306                                params.setLoadSynchronousUpTo(1);
307                                params.add("url", new UriParam(theUri));
308                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
309                }
310
311                Integer size = search.size();
312                if (size == null || size == 0) {
313                        return myNoMatch;
314                }
315
316                if (size > 1) {
317                        ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
318                }
319
320                return search.getResources(0, 1).get(0);
321        }
322
323        @Override
324        public FhirContext getFhirContext() {
325                return myFhirContext;
326        }
327
328        @PostConstruct
329        public void start() {
330                myStructureDefinitionType = myFhirContext.getResourceDefinition("StructureDefinition").getImplementingClass();
331                myValueSetType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
332
333                if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
334                        myCodeSystemType = myFhirContext.getResourceDefinition("CodeSystem").getImplementingClass();
335                } else {
336                        myCodeSystemType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
337                }
338        }
339
340
341        public void clearCaches() {
342                myLoadCache.invalidateAll();
343        }
344}