
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}