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