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; 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 int versionSeparator = theUri.lastIndexOf('|'); 239 if (versionSeparator != -1) { 240 params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 241 params.add(StructureDefinition.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 242 } else { 243 params.add(StructureDefinition.SP_URL, new UriParam(theUri)); 244 } 245 search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); 246 break; 247 } 248 case "Questionnaire": { 249 SearchParameterMap params = new SearchParameterMap(); 250 params.setLoadSynchronousUpTo(1); 251 if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { 252 params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); 253 } else { 254 params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); 255 } 256 search = myDaoRegistry.getResourceDao("Questionnaire").search(params); 257 break; 258 } 259 case "CodeSystem": { 260 int versionSeparator = theUri.lastIndexOf('|'); 261 SearchParameterMap params = new SearchParameterMap(); 262 params.setLoadSynchronousUpTo(1); 263 if (versionSeparator != -1) { 264 params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 265 params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 266 } else { 267 params.add(CodeSystem.SP_URL, new UriParam(theUri)); 268 } 269 params.setSort(new SortSpec(SP_RES_LAST_UPDATED).setOrder(SortOrderEnum.DESC)); 270 search = myDaoRegistry.getResourceDao(resourceName).search(params); 271 break; 272 } 273 case "ImplementationGuide": 274 case "SearchParameter": { 275 SearchParameterMap params = new SearchParameterMap(); 276 params.setLoadSynchronousUpTo(1); 277 params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); 278 search = myDaoRegistry.getResourceDao(resourceName).search(params); 279 break; 280 } 281 default: 282 // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question 283 // has a URL 284 SearchParameterMap params = new SearchParameterMap(); 285 params.setLoadSynchronousUpTo(1); 286 params.add("url", new UriParam(theUri)); 287 search = myDaoRegistry.getResourceDao(resourceName).search(params); 288 } 289 290 Integer size = search.size(); 291 if (size == null || size == 0) { 292 return null; 293 } 294 295 if (size > 1) { 296 ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); 297 } 298 299 return search.getResources(0, 1).get(0); 300 } 301 302 @Override 303 public FhirContext getFhirContext() { 304 return myFhirContext; 305 } 306 307 @PostConstruct 308 public void start() { 309 myStructureDefinitionType = 310 myFhirContext.getResourceDefinition("StructureDefinition").getImplementingClass(); 311 myValueSetType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass(); 312 313 if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { 314 myCodeSystemType = myFhirContext.getResourceDefinition("CodeSystem").getImplementingClass(); 315 } else { 316 myCodeSystemType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass(); 317 } 318 } 319}