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