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 java.util.Arrays;
056import java.util.List;
057import java.util.Optional;
058import java.util.concurrent.TimeUnit;
059import java.util.function.Supplier;
060import javax.annotation.Nullable;
061import javax.annotation.PostConstruct;
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        @Override
107        public IBaseResource fetchCodeSystem(String theSystem) {
108                if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) {
109                        Optional<IBaseResource> currentCSOpt = getCodeSystemCurrentVersion(new UriType(theSystem));
110                        if (!currentCSOpt.isPresent()) {
111                                ourLog.info("Couldn't find current version of CodeSystem: " + theSystem);
112                        }
113                        return currentCSOpt.orElse(null);
114                }
115
116                return fetchResource(myCodeSystemType, theSystem);
117        }
118
119        /**
120         * Obtains the current version of a CodeSystem using the fact that the current
121         * version is always pointed by the ForcedId for the no-versioned CS
122         */
123        private Optional<IBaseResource> getCodeSystemCurrentVersion(UriType theUrl) {
124                if (!theUrl.getValueAsString().contains(LOINC_LOW)) {
125                        return Optional.empty();
126                }
127
128                return myTermReadSvc.readCodeSystemByForcedId(LOINC_LOW);
129        }
130
131        @Override
132        public IBaseResource fetchValueSet(String theSystem) {
133                if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) {
134                        Optional<IBaseResource> currentVSOpt = getValueSetCurrentVersion(new UriType(theSystem));
135                        return currentVSOpt.orElse(null);
136                }
137
138                return fetchResource(myValueSetType, theSystem);
139        }
140
141        /**
142         * Obtains the current version of a ValueSet using the fact that the current
143         * version is always pointed by the ForcedId for the no-versioned VS
144         */
145        private Optional<IBaseResource> getValueSetCurrentVersion(UriType theUrl) {
146                Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl.getValueAsString());
147                if (!vsIdOpt.isPresent()) {
148                        return Optional.empty();
149                }
150
151                IFhirResourceDao<? extends IBaseResource> valueSetResourceDao = myDaoRegistry.getResourceDao(myValueSetType);
152                IBaseResource valueSet = valueSetResourceDao.read(new IdDt("ValueSet", vsIdOpt.get()));
153                return Optional.ofNullable(valueSet);
154        }
155
156        @Override
157        public IBaseResource fetchStructureDefinition(String theUrl) {
158                return fetchResource(myStructureDefinitionType, theUrl);
159        }
160
161        @SuppressWarnings("unchecked")
162        @Nullable
163        @Override
164        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
165                if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) {
166                        return null;
167                }
168                IBundleProvider search = myDaoRegistry
169                                .getResourceDao("StructureDefinition")
170                                .search(new SearchParameterMap().setLoadSynchronousUpTo(1000));
171                return (List<T>) search.getResources(0, 1000);
172        }
173
174        @Override
175        @SuppressWarnings({"unchecked", "unused"})
176        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
177                if (isBlank(theUri)) {
178                        return null;
179                }
180
181                String key = theClass + " " + theUri;
182                IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri));
183
184                if (fetched == myNoMatch) {
185                        return null;
186                }
187
188                return (T) fetched;
189        }
190
191        private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) {
192                if (theClass == null) {
193                        Supplier<IBaseResource>[] fetchers = new Supplier[] {
194                                () -> doFetchResource(ValueSet.class, theUri),
195                                () -> doFetchResource(CodeSystem.class, theUri),
196                                () -> doFetchResource(StructureDefinition.class, theUri)
197                        };
198                        return Arrays.stream(fetchers)
199                                        .map(t -> t.get())
200                                        .filter(t -> t != myNoMatch)
201                                        .findFirst()
202                                        .orElse(myNoMatch);
203                }
204
205                IdType id = new IdType(theUri);
206                boolean localReference = false;
207                if (id.hasBaseUrl() == false && id.hasIdPart() == true) {
208                        localReference = true;
209                }
210
211                String resourceName = myFhirContext.getResourceType(theClass);
212                IBundleProvider search;
213                switch (resourceName) {
214                        case "ValueSet":
215                                if (localReference) {
216                                        SearchParameterMap params = new SearchParameterMap();
217                                        params.setLoadSynchronousUpTo(1);
218                                        params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
219                                        search = myDaoRegistry.getResourceDao(resourceName).search(params);
220                                        if (search.size() == 0) {
221                                                params = new SearchParameterMap();
222                                                params.setLoadSynchronousUpTo(1);
223                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
224                                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
225                                        }
226                                } else {
227                                        int versionSeparator = theUri.lastIndexOf('|');
228                                        SearchParameterMap params = new SearchParameterMap();
229                                        params.setLoadSynchronousUpTo(1);
230                                        if (versionSeparator != -1) {
231                                                params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
232                                                params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
233                                        } else {
234                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
235                                        }
236                                        params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
237                                        search = myDaoRegistry.getResourceDao(resourceName).search(params);
238
239                                        if (search.isEmpty()
240                                                        && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
241                                                params = new SearchParameterMap();
242                                                params.setLoadSynchronousUpTo(1);
243                                                if (versionSeparator != -1) {
244                                                        params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
245                                                        params.add("system", new UriParam(theUri.substring(0, versionSeparator)));
246                                                } else {
247                                                        params.add("system", new UriParam(theUri));
248                                                }
249                                                params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
250                                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
251                                        }
252                                }
253                                break;
254                        case "StructureDefinition": {
255                                // Don't allow the core FHIR definitions to be overwritten
256                                if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
257                                        String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
258                                        if (myFhirContext.getElementDefinition(typeName) != null) {
259                                                return myNoMatch;
260                                        }
261                                }
262                                SearchParameterMap params = new SearchParameterMap();
263                                params.setLoadSynchronousUpTo(1);
264                                params.add(StructureDefinition.SP_URL, new UriParam(theUri));
265                                search = myDaoRegistry.getResourceDao("StructureDefinition").search(params);
266                                break;
267                        }
268                        case "Questionnaire": {
269                                SearchParameterMap params = new SearchParameterMap();
270                                params.setLoadSynchronousUpTo(1);
271                                if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
272                                        params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
273                                } else {
274                                        params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
275                                }
276                                search = myDaoRegistry.getResourceDao("Questionnaire").search(params);
277                                break;
278                        }
279                        case "CodeSystem": {
280                                int versionSeparator = theUri.lastIndexOf('|');
281                                SearchParameterMap params = new SearchParameterMap();
282                                params.setLoadSynchronousUpTo(1);
283                                if (versionSeparator != -1) {
284                                        params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
285                                        params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
286                                } else {
287                                        params.add(CodeSystem.SP_URL, new UriParam(theUri));
288                                }
289                                params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC));
290                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
291                                break;
292                        }
293                        case "ImplementationGuide":
294                        case "SearchParameter": {
295                                SearchParameterMap params = new SearchParameterMap();
296                                params.setLoadSynchronousUpTo(1);
297                                params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
298                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
299                                break;
300                        }
301                        default:
302                                // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question
303                                // has a URL
304                                SearchParameterMap params = new SearchParameterMap();
305                                params.setLoadSynchronousUpTo(1);
306                                params.add("url", new UriParam(theUri));
307                                search = myDaoRegistry.getResourceDao(resourceName).search(params);
308                }
309
310                Integer size = search.size();
311                if (size == null || size == 0) {
312                        return myNoMatch;
313                }
314
315                if (size > 1) {
316                        ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
317                }
318
319                return search.getResources(0, 1).get(0);
320        }
321
322        @Override
323        public FhirContext getFhirContext() {
324                return myFhirContext;
325        }
326
327        @PostConstruct
328        public void start() {
329                myStructureDefinitionType =
330                                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        public void clearCaches() {
341                myLoadCache.invalidateAll();
342        }
343}