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.jpa.term.api.ITermReadSvc; 030import ca.uhn.fhir.model.primitive.IdDt; 031import ca.uhn.fhir.rest.api.SortOrderEnum; 032import ca.uhn.fhir.rest.api.SortSpec; 033import ca.uhn.fhir.rest.api.server.IBundleProvider; 034import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 035import ca.uhn.fhir.rest.param.StringParam; 036import ca.uhn.fhir.rest.param.TokenParam; 037import ca.uhn.fhir.rest.param.UriParam; 038import ca.uhn.fhir.sl.cache.Cache; 039import ca.uhn.fhir.sl.cache.CacheFactory; 040import jakarta.annotation.Nullable; 041import jakarta.annotation.PostConstruct; 042import org.apache.commons.lang3.Validate; 043import org.hl7.fhir.instance.model.api.IAnyResource; 044import org.hl7.fhir.instance.model.api.IBaseResource; 045import org.hl7.fhir.r4.model.CodeSystem; 046import org.hl7.fhir.r4.model.IdType; 047import org.hl7.fhir.r4.model.ImplementationGuide; 048import org.hl7.fhir.r4.model.Questionnaire; 049import org.hl7.fhir.r4.model.StructureDefinition; 050import org.hl7.fhir.r4.model.UriType; 051import org.hl7.fhir.r4.model.ValueSet; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054import org.springframework.beans.factory.annotation.Autowired; 055import org.springframework.transaction.annotation.Propagation; 056import org.springframework.transaction.annotation.Transactional; 057 058import java.util.Arrays; 059import java.util.List; 060import java.util.Optional; 061import java.util.concurrent.TimeUnit; 062import java.util.function.Supplier; 063 064import static org.apache.commons.lang3.StringUtils.isBlank; 065import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW; 066 067/** 068 * This class is a {@link IValidationSupport Validation support} module that loads 069 * validation resources (StructureDefinition, ValueSet, CodeSystem, etc.) from the resources 070 * persisted in the JPA server. 071 */ 072@Transactional(propagation = Propagation.REQUIRED) 073public class JpaPersistedResourceValidationSupport implements IValidationSupport { 074 075 private static final Logger ourLog = LoggerFactory.getLogger(JpaPersistedResourceValidationSupport.class); 076 077 private final FhirContext myFhirContext; 078 private final IBaseResource myNoMatch; 079 080 @Autowired 081 private DaoRegistry myDaoRegistry; 082 083 @Autowired 084 private ITermReadSvc myTermReadSvc; 085 086 private Class<? extends IBaseResource> myCodeSystemType; 087 private Class<? extends IBaseResource> myStructureDefinitionType; 088 private Class<? extends IBaseResource> myValueSetType; 089 090 // TODO: JA2 We shouldn't need to cache here, but we probably still should since the 091 // TermReadSvcImpl calls these methods as a part of its "isCodeSystemSupported" calls. 092 // We should modify CachingValidationSupport to cache the results of "isXXXSupported" 093 // at which point we could do away with this cache 094 // TODO: LD: This cache seems to supersede the cache in CachingValidationSupport, as that cache is set to 095 // 10 minutes, but this 1 minute cache now determines the expiry. 096 // This new behaviour was introduced between the 7.0.0 release and the current master (7.2.0) 097 private Cache<String, IBaseResource> myLoadCache = CacheFactory.build(TimeUnit.MINUTES.toMillis(1), 1000); 098 099 /** 100 * Constructor 101 */ 102 public JpaPersistedResourceValidationSupport(FhirContext theFhirContext) { 103 super(); 104 Validate.notNull(theFhirContext); 105 myFhirContext = theFhirContext; 106 107 myNoMatch = myFhirContext.getResourceDefinition("Basic").newInstance(); 108 } 109 110 @Override 111 public String getName() { 112 return myFhirContext.getVersion().getVersion() + " JPA Validation Support"; 113 } 114 115 @Override 116 public IBaseResource fetchCodeSystem(String theSystem) { 117 if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) { 118 Optional<IBaseResource> currentCSOpt = getCodeSystemCurrentVersion(new UriType(theSystem)); 119 if (!currentCSOpt.isPresent()) { 120 ourLog.info("Couldn't find current version of CodeSystem: " + theSystem); 121 } 122 return currentCSOpt.orElse(null); 123 } 124 125 return fetchResource(myCodeSystemType, theSystem); 126 } 127 128 /** 129 * Obtains the current version of a CodeSystem using the fact that the current 130 * version is always pointed by the ForcedId for the no-versioned CS 131 */ 132 private Optional<IBaseResource> getCodeSystemCurrentVersion(UriType theUrl) { 133 if (!theUrl.getValueAsString().contains(LOINC_LOW)) { 134 return Optional.empty(); 135 } 136 137 return myTermReadSvc.readCodeSystemByForcedId(LOINC_LOW); 138 } 139 140 @Override 141 public IBaseResource fetchValueSet(String theSystem) { 142 if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) { 143 Optional<IBaseResource> currentVSOpt = getValueSetCurrentVersion(new UriType(theSystem)); 144 return currentVSOpt.orElse(null); 145 } 146 147 return fetchResource(myValueSetType, theSystem); 148 } 149 150 /** 151 * Obtains the current version of a ValueSet using the fact that the current 152 * version is always pointed by the ForcedId for the no-versioned VS 153 */ 154 private Optional<IBaseResource> getValueSetCurrentVersion(UriType theUrl) { 155 Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl.getValueAsString()); 156 if (!vsIdOpt.isPresent()) { 157 return Optional.empty(); 158 } 159 160 IFhirResourceDao<? extends IBaseResource> valueSetResourceDao = myDaoRegistry.getResourceDao(myValueSetType); 161 IBaseResource valueSet = valueSetResourceDao.read(new IdDt("ValueSet", vsIdOpt.get())); 162 return Optional.ofNullable(valueSet); 163 } 164 165 @Override 166 public IBaseResource fetchStructureDefinition(String theUrl) { 167 return fetchResource(myStructureDefinitionType, theUrl); 168 } 169 170 @SuppressWarnings("unchecked") 171 @Nullable 172 @Override 173 public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { 174 if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) { 175 return null; 176 } 177 IBundleProvider search = myDaoRegistry 178 .getResourceDao("StructureDefinition") 179 .search(new SearchParameterMap().setLoadSynchronousUpTo(1000), new SystemRequestDetails()); 180 return (List<T>) search.getResources(0, 1000); 181 } 182 183 @Override 184 @SuppressWarnings({"unchecked", "unused"}) 185 public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) { 186 if (isBlank(theUri)) { 187 return null; 188 } 189 190 String key = theClass + " " + theUri; 191 IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri)); 192 193 if (fetched == myNoMatch) { 194 ourLog.debug( 195 "Invalidating cache entry for URI: {} since the result of the underlying query is empty", theUri); 196 myLoadCache.invalidate(key); 197 return null; 198 } 199 200 return (T) fetched; 201 } 202 203 private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) { 204 if (theClass == null) { 205 Supplier<IBaseResource>[] fetchers = new Supplier[] { 206 () -> doFetchResource(ValueSet.class, theUri), 207 () -> doFetchResource(CodeSystem.class, theUri), 208 () -> doFetchResource(StructureDefinition.class, theUri) 209 }; 210 return Arrays.stream(fetchers) 211 .map(t -> t.get()) 212 .filter(t -> t != myNoMatch) 213 .findFirst() 214 .orElse(myNoMatch); 215 } 216 217 IdType id = new IdType(theUri); 218 boolean localReference = false; 219 if (id.hasBaseUrl() == false && id.hasIdPart() == true) { 220 localReference = true; 221 } 222 223 String resourceName = myFhirContext.getResourceType(theClass); 224 IBundleProvider search; 225 switch (resourceName) { 226 case "ValueSet": 227 if (localReference) { 228 SearchParameterMap params = new SearchParameterMap(); 229 params.setLoadSynchronousUpTo(1); 230 params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); 231 search = myDaoRegistry.getResourceDao(resourceName).search(params); 232 if (search.size() == 0) { 233 params = new SearchParameterMap(); 234 params.setLoadSynchronousUpTo(1); 235 params.add(ValueSet.SP_URL, new UriParam(theUri)); 236 search = myDaoRegistry.getResourceDao(resourceName).search(params); 237 } 238 } else { 239 int versionSeparator = theUri.lastIndexOf('|'); 240 SearchParameterMap params = new SearchParameterMap(); 241 params.setLoadSynchronousUpTo(1); 242 if (versionSeparator != -1) { 243 params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 244 params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 245 } else { 246 params.add(ValueSet.SP_URL, new UriParam(theUri)); 247 } 248 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 249 search = myDaoRegistry.getResourceDao(resourceName).search(params); 250 251 if (search.isEmpty() 252 && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 253 params = new SearchParameterMap(); 254 params.setLoadSynchronousUpTo(1); 255 if (versionSeparator != -1) { 256 params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 257 params.add("system", new UriParam(theUri.substring(0, versionSeparator))); 258 } else { 259 params.add("system", new UriParam(theUri)); 260 } 261 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 262 search = myDaoRegistry.getResourceDao(resourceName).search(params); 263 } 264 } 265 break; 266 case "StructureDefinition": { 267 // Don't allow the core FHIR definitions to be overwritten 268 if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { 269 String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); 270 if (myFhirContext.getElementDefinition(typeName) != null) { 271 return myNoMatch; 272 } 273 } 274 SearchParameterMap params = new SearchParameterMap(); 275 params.setLoadSynchronousUpTo(1); 276 params.add(StructureDefinition.SP_URL, new UriParam(theUri)); 277 search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); 278 break; 279 } 280 case "Questionnaire": { 281 SearchParameterMap params = new SearchParameterMap(); 282 params.setLoadSynchronousUpTo(1); 283 if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { 284 params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); 285 } else { 286 params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); 287 } 288 search = myDaoRegistry.getResourceDao("Questionnaire").search(params); 289 break; 290 } 291 case "CodeSystem": { 292 int versionSeparator = theUri.lastIndexOf('|'); 293 SearchParameterMap params = new SearchParameterMap(); 294 params.setLoadSynchronousUpTo(1); 295 if (versionSeparator != -1) { 296 params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 297 params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 298 } else { 299 params.add(CodeSystem.SP_URL, new UriParam(theUri)); 300 } 301 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 302 search = myDaoRegistry.getResourceDao(resourceName).search(params); 303 break; 304 } 305 case "ImplementationGuide": 306 case "SearchParameter": { 307 SearchParameterMap params = new SearchParameterMap(); 308 params.setLoadSynchronousUpTo(1); 309 params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); 310 search = myDaoRegistry.getResourceDao(resourceName).search(params); 311 break; 312 } 313 default: 314 // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question 315 // has a URL 316 SearchParameterMap params = new SearchParameterMap(); 317 params.setLoadSynchronousUpTo(1); 318 params.add("url", new UriParam(theUri)); 319 search = myDaoRegistry.getResourceDao(resourceName).search(params); 320 } 321 322 Integer size = search.size(); 323 if (size == null || size == 0) { 324 return myNoMatch; 325 } 326 327 if (size > 1) { 328 ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); 329 } 330 331 return search.getResources(0, 1).get(0); 332 } 333 334 @Override 335 public FhirContext getFhirContext() { 336 return myFhirContext; 337 } 338 339 @PostConstruct 340 public void start() { 341 myStructureDefinitionType = 342 myFhirContext.getResourceDefinition("StructureDefinition").getImplementingClass(); 343 myValueSetType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass(); 344 345 if (myFhirContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) { 346 myCodeSystemType = myFhirContext.getResourceDefinition("CodeSystem").getImplementingClass(); 347 } else { 348 myCodeSystemType = myFhirContext.getResourceDefinition("ValueSet").getImplementingClass(); 349 } 350 } 351 352 public void clearCaches() { 353 myLoadCache.invalidateAll(); 354 } 355}