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 java.util.Arrays; 056import java.util.List; 057import java.util.Optional; 058import java.util.concurrent.TimeUnit; 059import java.util.function.Supplier; 060import javax.annotation.Nullable; 061import javax.annotation.PostConstruct; 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 @Override 107 public IBaseResource fetchCodeSystem(String theSystem) { 108 if (TermReadSvcUtil.isLoincUnversionedCodeSystem(theSystem)) { 109 Optional<IBaseResource> currentCSOpt = getCodeSystemCurrentVersion(new UriType(theSystem)); 110 if (!currentCSOpt.isPresent()) { 111 ourLog.info("Couldn't find current version of CodeSystem: " + theSystem); 112 } 113 return currentCSOpt.orElse(null); 114 } 115 116 return fetchResource(myCodeSystemType, theSystem); 117 } 118 119 /** 120 * Obtains the current version of a CodeSystem using the fact that the current 121 * version is always pointed by the ForcedId for the no-versioned CS 122 */ 123 private Optional<IBaseResource> getCodeSystemCurrentVersion(UriType theUrl) { 124 if (!theUrl.getValueAsString().contains(LOINC_LOW)) { 125 return Optional.empty(); 126 } 127 128 return myTermReadSvc.readCodeSystemByForcedId(LOINC_LOW); 129 } 130 131 @Override 132 public IBaseResource fetchValueSet(String theSystem) { 133 if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) { 134 Optional<IBaseResource> currentVSOpt = getValueSetCurrentVersion(new UriType(theSystem)); 135 return currentVSOpt.orElse(null); 136 } 137 138 return fetchResource(myValueSetType, theSystem); 139 } 140 141 /** 142 * Obtains the current version of a ValueSet using the fact that the current 143 * version is always pointed by the ForcedId for the no-versioned VS 144 */ 145 private Optional<IBaseResource> getValueSetCurrentVersion(UriType theUrl) { 146 Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl.getValueAsString()); 147 if (!vsIdOpt.isPresent()) { 148 return Optional.empty(); 149 } 150 151 IFhirResourceDao<? extends IBaseResource> valueSetResourceDao = myDaoRegistry.getResourceDao(myValueSetType); 152 IBaseResource valueSet = valueSetResourceDao.read(new IdDt("ValueSet", vsIdOpt.get())); 153 return Optional.ofNullable(valueSet); 154 } 155 156 @Override 157 public IBaseResource fetchStructureDefinition(String theUrl) { 158 return fetchResource(myStructureDefinitionType, theUrl); 159 } 160 161 @SuppressWarnings("unchecked") 162 @Nullable 163 @Override 164 public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { 165 if (!myDaoRegistry.isResourceTypeSupported("StructureDefinition")) { 166 return null; 167 } 168 IBundleProvider search = myDaoRegistry 169 .getResourceDao("StructureDefinition") 170 .search(new SearchParameterMap().setLoadSynchronousUpTo(1000)); 171 return (List<T>) search.getResources(0, 1000); 172 } 173 174 @Override 175 @SuppressWarnings({"unchecked", "unused"}) 176 public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) { 177 if (isBlank(theUri)) { 178 return null; 179 } 180 181 String key = theClass + " " + theUri; 182 IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri)); 183 184 if (fetched == myNoMatch) { 185 return null; 186 } 187 188 return (T) fetched; 189 } 190 191 private <T extends IBaseResource> IBaseResource doFetchResource(@Nullable Class<T> theClass, String theUri) { 192 if (theClass == null) { 193 Supplier<IBaseResource>[] fetchers = new Supplier[] { 194 () -> doFetchResource(ValueSet.class, theUri), 195 () -> doFetchResource(CodeSystem.class, theUri), 196 () -> doFetchResource(StructureDefinition.class, theUri) 197 }; 198 return Arrays.stream(fetchers) 199 .map(t -> t.get()) 200 .filter(t -> t != myNoMatch) 201 .findFirst() 202 .orElse(myNoMatch); 203 } 204 205 IdType id = new IdType(theUri); 206 boolean localReference = false; 207 if (id.hasBaseUrl() == false && id.hasIdPart() == true) { 208 localReference = true; 209 } 210 211 String resourceName = myFhirContext.getResourceType(theClass); 212 IBundleProvider search; 213 switch (resourceName) { 214 case "ValueSet": 215 if (localReference) { 216 SearchParameterMap params = new SearchParameterMap(); 217 params.setLoadSynchronousUpTo(1); 218 params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); 219 search = myDaoRegistry.getResourceDao(resourceName).search(params); 220 if (search.size() == 0) { 221 params = new SearchParameterMap(); 222 params.setLoadSynchronousUpTo(1); 223 params.add(ValueSet.SP_URL, new UriParam(theUri)); 224 search = myDaoRegistry.getResourceDao(resourceName).search(params); 225 } 226 } else { 227 int versionSeparator = theUri.lastIndexOf('|'); 228 SearchParameterMap params = new SearchParameterMap(); 229 params.setLoadSynchronousUpTo(1); 230 if (versionSeparator != -1) { 231 params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 232 params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 233 } else { 234 params.add(ValueSet.SP_URL, new UriParam(theUri)); 235 } 236 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 237 search = myDaoRegistry.getResourceDao(resourceName).search(params); 238 239 if (search.isEmpty() 240 && myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 241 params = new SearchParameterMap(); 242 params.setLoadSynchronousUpTo(1); 243 if (versionSeparator != -1) { 244 params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 245 params.add("system", new UriParam(theUri.substring(0, versionSeparator))); 246 } else { 247 params.add("system", new UriParam(theUri)); 248 } 249 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 250 search = myDaoRegistry.getResourceDao(resourceName).search(params); 251 } 252 } 253 break; 254 case "StructureDefinition": { 255 // Don't allow the core FHIR definitions to be overwritten 256 if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { 257 String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); 258 if (myFhirContext.getElementDefinition(typeName) != null) { 259 return myNoMatch; 260 } 261 } 262 SearchParameterMap params = new SearchParameterMap(); 263 params.setLoadSynchronousUpTo(1); 264 params.add(StructureDefinition.SP_URL, new UriParam(theUri)); 265 search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); 266 break; 267 } 268 case "Questionnaire": { 269 SearchParameterMap params = new SearchParameterMap(); 270 params.setLoadSynchronousUpTo(1); 271 if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { 272 params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); 273 } else { 274 params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); 275 } 276 search = myDaoRegistry.getResourceDao("Questionnaire").search(params); 277 break; 278 } 279 case "CodeSystem": { 280 int versionSeparator = theUri.lastIndexOf('|'); 281 SearchParameterMap params = new SearchParameterMap(); 282 params.setLoadSynchronousUpTo(1); 283 if (versionSeparator != -1) { 284 params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); 285 params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); 286 } else { 287 params.add(CodeSystem.SP_URL, new UriParam(theUri)); 288 } 289 params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); 290 search = myDaoRegistry.getResourceDao(resourceName).search(params); 291 break; 292 } 293 case "ImplementationGuide": 294 case "SearchParameter": { 295 SearchParameterMap params = new SearchParameterMap(); 296 params.setLoadSynchronousUpTo(1); 297 params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); 298 search = myDaoRegistry.getResourceDao(resourceName).search(params); 299 break; 300 } 301 default: 302 // N.B.: this code assumes that we are searching by canonical URL and that the CanonicalType in question 303 // has a URL 304 SearchParameterMap params = new SearchParameterMap(); 305 params.setLoadSynchronousUpTo(1); 306 params.add("url", new UriParam(theUri)); 307 search = myDaoRegistry.getResourceDao(resourceName).search(params); 308 } 309 310 Integer size = search.size(); 311 if (size == null || size == 0) { 312 return myNoMatch; 313 } 314 315 if (size > 1) { 316 ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); 317 } 318 319 return search.getResources(0, 1).get(0); 320 } 321 322 @Override 323 public FhirContext getFhirContext() { 324 return myFhirContext; 325 } 326 327 @PostConstruct 328 public void start() { 329 myStructureDefinitionType = 330 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 public void clearCaches() { 341 myLoadCache.invalidateAll(); 342 } 343}