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