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