001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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.rest.api.SortOrderEnum;
030import ca.uhn.fhir.rest.api.SortSpec;
031import ca.uhn.fhir.rest.api.server.IBundleProvider;
032import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
033import ca.uhn.fhir.rest.param.StringParam;
034import ca.uhn.fhir.rest.param.TokenParam;
035import ca.uhn.fhir.rest.param.UriParam;
036import jakarta.annotation.Nullable;
037import jakarta.annotation.PostConstruct;
038import org.apache.commons.lang3.Validate;
039import org.hl7.fhir.instance.model.api.IAnyResource;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.hl7.fhir.instance.model.api.IIdType;
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.ValueSet;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import java.util.List;
052import java.util.Objects;
053import java.util.Optional;
054import java.util.function.Supplier;
055
056import static org.apache.commons.lang3.StringUtils.isBlank;
057import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW;
058import static org.hl7.fhir.instance.model.api.IAnyResource.SP_RES_LAST_UPDATED;
059
060/**
061 * This class is a {@link IValidationSupport Validation support} module that loads
062 * validation resources (StructureDefinition, ValueSet, CodeSystem, etc.) from the resources
063 * persisted in the JPA server.
064 *
065 * Note that this class is aware of the resource business version (not to be confused with the FHIR version or
066 * meta.versionId) for CodeSystem, ValueSet, and StructureDefinition resources.
067 * For example, a request for <code>http://example.com/StructureDefinition/ABC|1.2.3</code> will
068 * return the resource that matches the URL http://example.com/StructureDefinition/ABC and version 1.2.3.
069 * Unversioned URLs will match the most recently updated resource by using the meta.lastUpdated field.
070 *
071 */
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 DaoRegistry myDaoRegistry;
078
079        private Class<? extends IBaseResource> myCodeSystemType;
080        private Class<? extends IBaseResource> myStructureDefinitionType;
081        private Class<? extends IBaseResource> myValueSetType;
082
083        /**
084         * Constructor
085         */
086        public JpaPersistedResourceValidationSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
087                super();
088                Validate.notNull(theFhirContext, "theFhirContext must not be null");
089                Validate.notNull(theDaoRegistry, "theDaoRegistry must not be null");
090                myFhirContext = theFhirContext;
091                myDaoRegistry = theDaoRegistry;
092        }
093
094        @Override
095        public String getName() {
096                return myFhirContext.getVersion().getVersion() + " JPA Validation Support";
097        }
098
099        @Override
100        public IBaseResource fetchCodeSystem(String theSystem) {
101                if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) {
102                        IIdType id = myFhirContext.getVersion().newIdType("CodeSystem", LOINC_LOW);
103                        return findResourceByIdWithNoException(id, myCodeSystemType);
104                }
105
106                return fetchResource(myCodeSystemType, theSystem);
107        }
108
109        @Override
110        public IBaseResource fetchValueSet(String theSystem) {
111                if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) {
112                        Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theSystem);
113                        if (vsIdOpt.isEmpty()) {
114                                return null;
115                        }
116                        IIdType id = myFhirContext.getVersion().newIdType("ValueSet", vsIdOpt.get());
117                        return findResourceByIdWithNoException(id, myValueSetType);
118                }
119
120                return fetchResource(myValueSetType, theSystem);
121        }
122
123        /**
124         * Performs a lookup by ID, with no exception thrown (since that can mark the active
125         * transaction as rollback).
126         */
127        @Nullable
128        private IBaseResource findResourceByIdWithNoException(IIdType id, Class<? extends IBaseResource> type) {
129                SearchParameterMap map = SearchParameterMap.newSynchronous()
130                                .setLoadSynchronousUpTo(1)
131                                .add(IAnyResource.SP_RES_ID, new TokenParam(id.getValue()));
132                IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(type);
133                IBundleProvider outcome = dao.search(map, new SystemRequestDetails());
134                List<IBaseResource> resources = outcome.getResources(0, 1);
135                if (resources.isEmpty()) {
136                        return null;
137                } else {
138                        return resources.get(0);
139                }
140        }
141
142        @Override
143        public IBaseResource fetchStructureDefinition(String theUrl) {
144                assert myStructureDefinitionType != null;
145                return fetchResource(myStructureDefinitionType, theUrl);
146        }
147
148        @SuppressWarnings("unchecked")
149        @Nullable
150        @Override
151        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
152                if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) {
153                        return null;
154                }
155                IBundleProvider search = myDaoRegistry
156                                .getResourceDao("StructureDefinition")
157                                .search(new SearchParameterMap().setLoadSynchronousUpTo(1000), new SystemRequestDetails());
158                return (List<T>) search.getResources(0, 1000);
159        }
160
161        @Override
162        @SuppressWarnings({"unchecked", "unused"})
163        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
164                if (isBlank(theUri)) {
165                        return null;
166                }
167
168                return (T) doFetchResource(theClass, theUri);
169        }
170
171        private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) {
172                if (theClass == null) {
173                        List<Supplier<IBaseResource>> fetchers = List.of(
174                                        () -> doFetchResource(myValueSetType, theUri),
175                                        () -> doFetchResource(myCodeSystemType, theUri),
176                                        () -> doFetchResource(myStructureDefinitionType, theUri));
177                        return fetchers.stream()
178                                        .map(Supplier::get)
179                                        .filter(Objects::nonNull)
180                                        .findFirst()
181                                        .orElse(null);
182                }
183
184                IdType id = new IdType(theUri);
185                boolean localReference = id.hasBaseUrl() == false && id.hasIdPart() == true;
186
187                String resourceName = myFhirContext.getResourceType(theClass);
188                IBundleProvider search;
189                switch (resourceName) {
190                        case "ValueSet":
191                                if (localReference) {
192                                        SearchParameterMap params = new SearchParameterMap();
193                                        params.setLoadSynchronousUpTo(1);
194                                        params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
195                                        search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
196                                        if (search.isEmpty()) {
197                                                params = new SearchParameterMap();
198                                                params.setLoadSynchronousUpTo(1);
199                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
200                                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
201                                        }
202                                } else {
203                                        int versionSeparator = theUri.lastIndexOf('|');
204                                        SearchParameterMap params = new SearchParameterMap();
205                                        params.setLoadSynchronousUpTo(1);
206                                        if (versionSeparator != -1) {
207                                                params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
208                                                params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
209                                        } else {
210                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
211                                        }
212                                        params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
213                                        search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
214
215                                        if (search.isEmpty()
216                                                        && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
217                                                params = new SearchParameterMap();
218                                                params.setLoadSynchronousUpTo(1);
219                                                if (versionSeparator != -1) {
220                                                        params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
221                                                        params.add(
222                                                                        ca.uhn.fhir.model.dstu2.resource.ValueSet.SP_SYSTEM,
223                                                                        new UriParam(theUri.substring(0, versionSeparator)));
224                                                } else {
225                                                        params.add(ca.uhn.fhir.model.dstu2.resource.ValueSet.SP_SYSTEM, new UriParam(theUri));
226                                                }
227                                                params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
228                                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
229                                        }
230                                }
231                                break;
232                        case "StructureDefinition": {
233                                // Don't allow the core FHIR definitions to be overwritten
234                                if (theUri.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) {
235                                        String typeName = theUri.substring(URL_PREFIX_STRUCTURE_DEFINITION.length());
236                                        if (myFhirContext.getElementDefinition(typeName) != null) {
237                                                return null;
238                                        }
239                                }
240                                SearchParameterMap params = new SearchParameterMap();
241                                params.setLoadSynchronousUpTo(1);
242                                int versionSeparator = theUri.lastIndexOf('|');
243                                if (versionSeparator != -1) {
244                                        params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
245                                        params.add(StructureDefinition.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
246                                } else {
247                                        params.add(StructureDefinition.SP_URL, new UriParam(theUri));
248                                        // When no version is specified, we will take the most recently updated resource as the current
249                                        // version
250                                        params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
251                                }
252                                search = myDaoRegistry.getResourceDao("StructureDefinition").search(params, new SystemRequestDetails());
253                                break;
254                        }
255                        case "Questionnaire": {
256                                SearchParameterMap params = new SearchParameterMap();
257                                params.setLoadSynchronousUpTo(1);
258                                if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
259                                        params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
260                                } else {
261                                        params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
262                                }
263                                search = myDaoRegistry.getResourceDao("Questionnaire").search(params, new SystemRequestDetails());
264                                break;
265                        }
266                        case "CodeSystem": {
267                                int versionSeparator = theUri.lastIndexOf('|');
268                                SearchParameterMap params = new SearchParameterMap();
269                                params.setLoadSynchronousUpTo(1);
270                                if (versionSeparator != -1) {
271                                        params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
272                                        params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
273                                } else {
274                                        params.add(CodeSystem.SP_URL, new UriParam(theUri));
275                                }
276                                params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
277                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
278                                break;
279                        }
280                        case "ImplementationGuide":
281                        case "SearchParameter": {
282                                SearchParameterMap params = new SearchParameterMap();
283                                params.setLoadSynchronousUpTo(1);
284                                params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
285                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
286                                break;
287                        }
288                        default:
289                                // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question
290                                // has a URL
291                                SearchParameterMap params = new SearchParameterMap();
292                                params.setLoadSynchronousUpTo(1);
293                                params.add("url", new UriParam(theUri));
294                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
295                }
296
297                Integer size = search.size();
298                if (size == null || size == 0) {
299                        return null;
300                }
301
302                if (size > 1) {
303                        ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
304                }
305
306                return search.getResources(0, 1).get(0);
307        }
308
309        @Override
310        public FhirContext getFhirContext() {
311                return myFhirContext;
312        }
313
314        @PostConstruct
315        public void start() {
316                myStructureDefinitionType =
317                                myFhirContext.getResourceDefinition("StructureDefinition").getImplementingClass();
318                myValueSetType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
319
320                if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
321                        myCodeSystemType = myFhirContext.getResourceDefinition("CodeSystem").getImplementingClass();
322                } else {
323                        myCodeSystemType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
324                }
325        }
326}