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}