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 */
065public class JpaPersistedResourceValidationSupport implements IValidationSupport {
066
067        private static final Logger ourLog = LoggerFactory.getLogger(JpaPersistedResourceValidationSupport.class);
068
069        private final FhirContext myFhirContext;
070        private final DaoRegistry myDaoRegistry;
071
072        private Class<? extends IBaseResource> myCodeSystemType;
073        private Class<? extends IBaseResource> myStructureDefinitionType;
074        private Class<? extends IBaseResource> myValueSetType;
075
076        /**
077         * Constructor
078         */
079        public JpaPersistedResourceValidationSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry) {
080                super();
081                Validate.notNull(theFhirContext, "theFhirContext must not be null");
082                Validate.notNull(theDaoRegistry, "theDaoRegistry must not be null");
083                myFhirContext = theFhirContext;
084                myDaoRegistry = theDaoRegistry;
085        }
086
087        @Override
088        public String getName() {
089                return myFhirContext.getVersion().getVersion() + " JPA Validation Support";
090        }
091
092        @Override
093        public IBaseResource fetchCodeSystem(String theSystem) {
094                if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) {
095                        IIdType id = myFhirContext.getVersion().newIdType("CodeSystem", LOINC_LOW);
096                        return findResourceByIdWithNoException(id, myCodeSystemType);
097                }
098
099                return fetchResource(myCodeSystemType, theSystem);
100        }
101
102        @Override
103        public IBaseResource fetchValueSet(String theSystem) {
104                if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) {
105                        Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theSystem);
106                        if (vsIdOpt.isEmpty()) {
107                                return null;
108                        }
109                        IIdType id = myFhirContext.getVersion().newIdType("ValueSet", vsIdOpt.get());
110                        return findResourceByIdWithNoException(id, myValueSetType);
111                }
112
113                return fetchResource(myValueSetType, theSystem);
114        }
115
116        /**
117         * Performs a lookup by ID, with no exception thrown (since that can mark the active
118         * transaction as rollback).
119         */
120        @Nullable
121        private IBaseResource findResourceByIdWithNoException(IIdType id, Class<? extends IBaseResource> type) {
122                SearchParameterMap map = SearchParameterMap.newSynchronous()
123                                .setLoadSynchronousUpTo(1)
124                                .add(IAnyResource.SP_RES_ID, new TokenParam(id.getValue()));
125                IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(type);
126                IBundleProvider outcome = dao.search(map, new SystemRequestDetails());
127                List<IBaseResource> resources = outcome.getResources(0, 1);
128                if (resources.isEmpty()) {
129                        return null;
130                } else {
131                        return resources.get(0);
132                }
133        }
134
135        @Override
136        public IBaseResource fetchStructureDefinition(String theUrl) {
137                assert myStructureDefinitionType != null;
138                return fetchResource(myStructureDefinitionType, theUrl);
139        }
140
141        @SuppressWarnings("unchecked")
142        @Nullable
143        @Override
144        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
145                if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) {
146                        return null;
147                }
148                IBundleProvider search = myDaoRegistry
149                                .getResourceDao("StructureDefinition")
150                                .search(new SearchParameterMap().setLoadSynchronousUpTo(1000), new SystemRequestDetails());
151                return (List<T>) search.getResources(0, 1000);
152        }
153
154        @Override
155        @SuppressWarnings({"unchecked", "unused"})
156        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
157                if (isBlank(theUri)) {
158                        return null;
159                }
160
161                return (T) doFetchResource(theClass, theUri);
162        }
163
164        private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) {
165                if (theClass == null) {
166                        List<Supplier<IBaseResource>> fetchers = List.of(
167                                        () -> doFetchResource(myValueSetType, theUri),
168                                        () -> doFetchResource(myCodeSystemType, theUri),
169                                        () -> doFetchResource(myStructureDefinitionType, theUri));
170                        return fetchers.stream()
171                                        .map(Supplier::get)
172                                        .filter(Objects::nonNull)
173                                        .findFirst()
174                                        .orElse(null);
175                }
176
177                IdType id = new IdType(theUri);
178                boolean localReference = id.hasBaseUrl() == false && id.hasIdPart() == true;
179
180                String resourceName = myFhirContext.getResourceType(theClass);
181                IBundleProvider search;
182                switch (resourceName) {
183                        case "ValueSet":
184                                if (localReference) {
185                                        SearchParameterMap params = new SearchParameterMap();
186                                        params.setLoadSynchronousUpTo(1);
187                                        params.add(IAnyResource.SP_RES_ID, new StringParam(theUri));
188                                        search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
189                                        if (search.isEmpty()) {
190                                                params = new SearchParameterMap();
191                                                params.setLoadSynchronousUpTo(1);
192                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
193                                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
194                                        }
195                                } else {
196                                        int versionSeparator = theUri.lastIndexOf('|');
197                                        SearchParameterMap params = new SearchParameterMap();
198                                        params.setLoadSynchronousUpTo(1);
199                                        if (versionSeparator != -1) {
200                                                params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
201                                                params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
202                                        } else {
203                                                params.add(ValueSet.SP_URL, new UriParam(theUri));
204                                        }
205                                        params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
206                                        search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
207
208                                        if (search.isEmpty()
209                                                        && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
210                                                params = new SearchParameterMap();
211                                                params.setLoadSynchronousUpTo(1);
212                                                if (versionSeparator != -1) {
213                                                        params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
214                                                        params.add(
215                                                                        ca.uhn.fhir.model.dstu2.resource.ValueSet.SP_SYSTEM,
216                                                                        new UriParam(theUri.substring(0, versionSeparator)));
217                                                } else {
218                                                        params.add(ca.uhn.fhir.model.dstu2.resource.ValueSet.SP_SYSTEM, new UriParam(theUri));
219                                                }
220                                                params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
221                                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
222                                        }
223                                }
224                                break;
225                        case "StructureDefinition": {
226                                // Don't allow the core FHIR definitions to be overwritten
227                                if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
228                                        String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length());
229                                        if (myFhirContext.getElementDefinition(typeName) != null) {
230                                                return null;
231                                        }
232                                }
233                                SearchParameterMap params = new SearchParameterMap();
234                                params.setLoadSynchronousUpTo(1);
235                                int versionSeparator = theUri.lastIndexOf('|');
236                                if (versionSeparator != -1) {
237                                        params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
238                                        params.add(StructureDefinition.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
239                                } else {
240                                        params.add(StructureDefinition.SP_URL, new UriParam(theUri));
241                                }
242                                search = myDaoRegistry.getResourceDao("StructureDefinition").search(params, new SystemRequestDetails());
243                                break;
244                        }
245                        case "Questionnaire": {
246                                SearchParameterMap params = new SearchParameterMap();
247                                params.setLoadSynchronousUpTo(1);
248                                if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) {
249                                        params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart()));
250                                } else {
251                                        params.add(Questionnaire.SP_URL, new UriParam(id.getValue()));
252                                }
253                                search = myDaoRegistry.getResourceDao("Questionnaire").search(params, new SystemRequestDetails());
254                                break;
255                        }
256                        case "CodeSystem": {
257                                int versionSeparator = theUri.lastIndexOf('|');
258                                SearchParameterMap params = new SearchParameterMap();
259                                params.setLoadSynchronousUpTo(1);
260                                if (versionSeparator != -1) {
261                                        params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1)));
262                                        params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator)));
263                                } else {
264                                        params.add(CodeSystem.SP_URL, new UriParam(theUri));
265                                }
266                                params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC));
267                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
268                                break;
269                        }
270                        case "ImplementationGuide":
271                        case "SearchParameter": {
272                                SearchParameterMap params = new SearchParameterMap();
273                                params.setLoadSynchronousUpTo(1);
274                                params.add(ImplementationGuide.SP_URL, new UriParam(theUri));
275                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
276                                break;
277                        }
278                        default:
279                                // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question
280                                // has a URL
281                                SearchParameterMap params = new SearchParameterMap();
282                                params.setLoadSynchronousUpTo(1);
283                                params.add("url", new UriParam(theUri));
284                                search = myDaoRegistry.getResourceDao(resourceName).search(params, new SystemRequestDetails());
285                }
286
287                Integer size = search.size();
288                if (size == null || size == 0) {
289                        return null;
290                }
291
292                if (size > 1) {
293                        ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri);
294                }
295
296                return search.getResources(0, 1).get(0);
297        }
298
299        @Override
300        public FhirContext getFhirContext() {
301                return myFhirContext;
302        }
303
304        @PostConstruct
305        public void start() {
306                myStructureDefinitionType =
307                                myFhirContext.getResourceDefinition("StructureDefinition").getImplementingClass();
308                myValueSetType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
309
310                if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
311                        myCodeSystemType = myFhirContext.getResourceDefinition("CodeSystem").getImplementingClass();
312                } else {
313                        myCodeSystemType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass();
314                }
315        }
316}