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.ConceptValidationOptions; 025import ca.uhn.fhir.context.support.IValidationSupport; 026import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult; 027import ca.uhn.fhir.context.support.LookupCodeRequest; 028import ca.uhn.fhir.context.support.ValidationSupportContext; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 031import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; 032import ca.uhn.fhir.jpa.api.dao.ReindexOutcome; 033import ca.uhn.fhir.jpa.api.dao.ReindexParameters; 034import ca.uhn.fhir.jpa.api.model.ReindexJobStatus; 035import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 036import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 037import ca.uhn.fhir.jpa.model.entity.ResourceTable; 038import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 039import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; 040import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; 041import ca.uhn.fhir.jpa.util.LogicUtil; 042import ca.uhn.fhir.rest.api.server.RequestDetails; 043import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 044import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 045import ca.uhn.fhir.rest.param.TokenParam; 046import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 047import ca.uhn.fhir.util.FhirTerser; 048import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 049import jakarta.annotation.Nonnull; 050import jakarta.annotation.PostConstruct; 051import org.apache.commons.collections4.CollectionUtils; 052import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; 053import org.hl7.fhir.instance.model.api.IBaseCoding; 054import org.hl7.fhir.instance.model.api.IBaseDatatype; 055import org.hl7.fhir.instance.model.api.IBaseResource; 056import org.hl7.fhir.instance.model.api.IIdType; 057import org.hl7.fhir.instance.model.api.IPrimitiveType; 058import org.hl7.fhir.r4.model.CodeableConcept; 059import org.hl7.fhir.r4.model.Coding; 060import org.springframework.beans.factory.annotation.Autowired; 061 062import java.util.ArrayList; 063import java.util.Collection; 064import java.util.Date; 065import java.util.List; 066import java.util.stream.Collectors; 067 068import static ca.uhn.fhir.util.DatatypeUtil.toStringValue; 069import static org.apache.commons.lang3.StringUtils.isNotBlank; 070 071public class JpaResourceDaoCodeSystem<T extends IBaseResource> extends BaseHapiFhirResourceDao<T> 072 implements IFhirResourceDaoCodeSystem<T> { 073 074 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceDaoCodeSystem.class); 075 076 @Autowired 077 protected ITermCodeSystemStorageSvc myTerminologyCodeSystemStorageSvc; 078 079 @Autowired 080 protected IIdHelperService myIdHelperService; 081 082 @Autowired 083 protected ITermDeferredStorageSvc myTermDeferredStorageSvc; 084 085 @Autowired 086 private IValidationSupport myValidationSupport; 087 088 @Autowired 089 private FhirContext myFhirContext; 090 091 private FhirTerser myTerser; 092 093 @Autowired 094 private VersionCanonicalizer myVersionCanonicalizer; 095 096 @Override 097 @PostConstruct 098 public void start() { 099 super.start(); 100 myTerser = myFhirContext.newTerser(); 101 } 102 103 @Override 104 public List<IIdType> findCodeSystemIdsContainingSystemAndCode( 105 String theCode, String theSystem, RequestDetails theRequest) { 106 List<IIdType> valueSetIds; 107 List<IResourcePersistentId> ids = searchForIds( 108 new SearchParameterMap(org.hl7.fhir.r4.model.CodeSystem.SP_CODE, new TokenParam(theSystem, theCode)), 109 theRequest); 110 valueSetIds = new ArrayList<>(); 111 for (IResourcePersistentId next : ids) { 112 IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "CodeSystem", next); 113 valueSetIds.add(id); 114 } 115 return valueSetIds; 116 } 117 118 @Nonnull 119 @Override 120 public IValidationSupport.LookupCodeResult lookupCode( 121 IPrimitiveType<String> theCode, 122 IPrimitiveType<String> theSystem, 123 IBaseCoding theCoding, 124 RequestDetails theRequestDetails) { 125 return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails); 126 } 127 128 @Nonnull 129 @Override 130 public IValidationSupport.LookupCodeResult lookupCode( 131 IPrimitiveType<String> theCode, 132 IPrimitiveType<String> theSystem, 133 IBaseCoding theCoding, 134 IPrimitiveType<String> theDisplayLanguage, 135 RequestDetails theRequestDetails) { 136 return lookupCode( 137 theCode, 138 theSystem, 139 theCoding, 140 theDisplayLanguage, 141 CollectionUtils.emptyCollection(), 142 theRequestDetails); 143 } 144 145 @Nonnull 146 @Override 147 public IValidationSupport.LookupCodeResult lookupCode( 148 IPrimitiveType<String> theCode, 149 IPrimitiveType<String> theSystem, 150 IBaseCoding theCoding, 151 IPrimitiveType<String> theDisplayLanguage, 152 Collection<IPrimitiveType<String>> thePropertyNames, 153 RequestDetails theRequestDetails) { 154 return doLookupCode( 155 myFhirContext, 156 myTerser, 157 myValidationSupport, 158 theCode, 159 theSystem, 160 theCoding, 161 theDisplayLanguage, 162 thePropertyNames); 163 } 164 165 @Override 166 public SubsumesResult subsumes( 167 IPrimitiveType<String> theCodeA, 168 IPrimitiveType<String> theCodeB, 169 IPrimitiveType<String> theSystem, 170 IBaseCoding theCodingA, 171 IBaseCoding theCodingB, 172 RequestDetails theRequestDetails) { 173 return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB); 174 } 175 176 @Override 177 protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) { 178 super.preDelete(theResourceToDelete, theEntityToDelete, theRequestDetails); 179 180 myTermDeferredStorageSvc.deleteCodeSystemForResource(theEntityToDelete); 181 } 182 183 /** 184 * If there are more code systems to process 185 * than {@link JpaStorageSettings#getDeferIndexingForCodesystemsOfSize()}, 186 * then these codes will have their processing deferred (for a later time). 187 * 188 * This can result in future reindex steps *skipping* these code systems (if 189 * they're still deferred) and thus incorrect expansions resulting. 190 * 191 * So we override the reindex method for CodeSystems specifically to 192 * force reindex batch jobs to wait until all code systems are processed before 193 * moving on. 194 */ 195 @SuppressWarnings("rawtypes") 196 @Override 197 public ReindexOutcome reindex( 198 IResourcePersistentId thePid, 199 ReindexParameters theReindexParameters, 200 RequestDetails theRequest, 201 TransactionDetails theTransactionDetails) { 202 ReindexOutcome outcome = super.reindex(thePid, theReindexParameters, theRequest, theTransactionDetails); 203 204 if (outcome.getWarnings().isEmpty()) { 205 outcome.setHasPendingWork(true); 206 } 207 return outcome; 208 } 209 210 @Override 211 public ReindexJobStatus getReindexJobStatus() { 212 boolean isQueueEmpty = myTermDeferredStorageSvc.isStorageQueueEmpty(true); 213 214 ReindexJobStatus status = new ReindexJobStatus(); 215 status.setHasReindexWorkPending(!isQueueEmpty); 216 if (status.isHasReindexWorkPending()) { 217 // force a run 218 myTermDeferredStorageSvc.saveDeferred(); 219 } 220 221 return status; 222 } 223 224 @Override 225 public ResourceTable updateEntity( 226 RequestDetails theRequest, 227 IBaseResource theResource, 228 IBasePersistedResource theEntity, 229 Date theDeletedTimestampOrNull, 230 boolean thePerformIndexing, 231 boolean theUpdateVersion, 232 TransactionDetails theTransactionDetails, 233 boolean theForceUpdate, 234 boolean theCreateNewHistoryEntry) { 235 236 ResourceTable retVal = super.updateEntity( 237 theRequest, 238 theResource, 239 theEntity, 240 theDeletedTimestampOrNull, 241 thePerformIndexing, 242 theUpdateVersion, 243 theTransactionDetails, 244 theForceUpdate, 245 theCreateNewHistoryEntry); 246 if (thePerformIndexing) { 247 if (!retVal.isUnchangedInCurrentOperation()) { 248 249 org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource); 250 addPidToResource(theEntity, cs); 251 252 myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded( 253 cs, (ResourceTable) theEntity, theRequest); 254 } 255 256 /* 257 * Flushing for each stored resource hurts performance, but in this case 258 * it's justified because we don't expect people to be submitting 259 * CodeSystem resources at super high rates, and we need to have the 260 * various writes finished in case a second entry in the same transaction 261 * tries to create a duplicate codesystem. 262 */ 263 myEntityManager.flush(); 264 } 265 266 return retVal; 267 } 268 269 @Nonnull 270 @Override 271 public CodeValidationResult validateCode( 272 IIdType theCodeSystemId, 273 IPrimitiveType<String> theCodeSystemUrl, 274 IPrimitiveType<String> theVersion, 275 IPrimitiveType<String> theCode, 276 IPrimitiveType<String> theDisplay, 277 IBaseCoding theCoding, 278 IBaseDatatype theCodeableConcept, 279 RequestDetails theRequestDetails) { 280 281 CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept); 282 boolean haveCodeableConcept = 283 codeableConcept != null && codeableConcept.getCoding().size() > 0; 284 285 Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding); 286 boolean haveCoding = coding != null && !coding.isEmpty(); 287 288 String code = toStringValue(theCode); 289 boolean haveCode = isNotBlank(code); 290 291 if (!haveCodeableConcept && !haveCoding && !haveCode) { 292 throw new InvalidRequestException( 293 Msg.code(906) + "No code, coding, or codeableConcept provided to validate."); 294 } 295 if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { 296 throw new InvalidRequestException( 297 Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)"); 298 } 299 300 String codeSystemUrl; 301 if (theCodeSystemId != null) { 302 IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails); 303 codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem); 304 } else if (isNotBlank(toStringValue(theCodeSystemUrl))) { 305 codeSystemUrl = toStringValue(theCodeSystemUrl); 306 } else { 307 throw new InvalidRequestException(Msg.code(908) 308 + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate."); 309 } 310 311 if (haveCodeableConcept) { 312 CodeValidationResult anyValidation = null; 313 for (int i = 0; i < codeableConcept.getCoding().size(); i++) { 314 Coding nextCoding = codeableConcept.getCoding().get(i); 315 if (nextCoding.hasSystem()) { 316 if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) { 317 throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem() 318 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 319 } 320 codeSystemUrl = nextCoding.getSystem(); 321 } 322 code = nextCoding.getCode(); 323 String display = nextCoding.getDisplay(); 324 CodeValidationResult nextValidation = 325 codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 326 anyValidation = nextValidation; 327 if (nextValidation.isOk()) { 328 return nextValidation; 329 } 330 } 331 return anyValidation; 332 } else if (haveCoding) { 333 if (coding.hasSystem()) { 334 if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) { 335 throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem() 336 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 337 } 338 codeSystemUrl = coding.getSystem(); 339 } 340 code = coding.getCode(); 341 String display = coding.getDisplay(); 342 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 343 } else { 344 String display = toStringValue(theDisplay); 345 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 346 } 347 } 348 349 private CodeValidationResult codeSystemValidateCode( 350 String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) { 351 ValidationSupportContext context = new ValidationSupportContext(myValidationSupport); 352 ConceptValidationOptions options = new ConceptValidationOptions(); 353 options.setValidateDisplay(isNotBlank(theDisplay)); 354 355 String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion); 356 357 CodeValidationResult retVal = 358 myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null); 359 if (retVal == null) { 360 retVal = new CodeValidationResult(); 361 retVal.setMessage( 362 "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode); 363 } 364 return retVal; 365 } 366 367 public static IValidationSupport.LookupCodeResult doLookupCode( 368 FhirContext theFhirContext, 369 FhirTerser theFhirTerser, 370 IValidationSupport theValidationSupport, 371 IPrimitiveType<String> theCode, 372 IPrimitiveType<String> theSystem, 373 IBaseCoding theCoding, 374 IPrimitiveType<String> theDisplayLanguage, 375 Collection<IPrimitiveType<String>> thePropertyNames) { 376 boolean haveCoding = theCoding != null 377 && isNotBlank(extractCodingSystem(theCoding)) 378 && isNotBlank(extractCodingCode(theCoding)); 379 boolean haveCode = theCode != null && theCode.isEmpty() == false; 380 boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; 381 boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false; 382 383 if (!haveCoding && !(haveSystem && haveCode)) { 384 throw new InvalidRequestException( 385 Msg.code(1126) + "No code, coding, or codeableConcept provided to validate"); 386 } 387 if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) { 388 throw new InvalidRequestException( 389 Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)"); 390 } 391 392 String code; 393 String system; 394 if (haveCoding) { 395 code = extractCodingCode(theCoding); 396 system = extractCodingSystem(theCoding); 397 String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding); 398 if (isNotBlank(version)) { 399 system = system + "|" + version; 400 } 401 } else { 402 code = theCode.getValue(); 403 system = theSystem.getValue(); 404 } 405 406 String displayLanguage = null; 407 if (haveDisplayLanguage) { 408 displayLanguage = theDisplayLanguage.getValue(); 409 } 410 411 ourLog.info("Looking up {} / {}", system, code); 412 413 Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream() 414 .map(IPrimitiveType::getValueAsString) 415 .collect(Collectors.toSet()); 416 417 if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) { 418 419 ourLog.info("Code system {} is supported", system); 420 IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode( 421 new ValidationSupportContext(theValidationSupport), 422 new LookupCodeRequest(system, code, displayLanguage, propertyNames)); 423 if (retVal != null) { 424 return retVal; 425 } 426 } 427 428 // We didn't find it.. 429 return IValidationSupport.LookupCodeResult.notFound(system, code); 430 } 431 432 private static String extractCodingSystem(IBaseCoding theCoding) { 433 return theCoding.getSystem(); 434 } 435 436 private static String extractCodingCode(IBaseCoding theCoding) { 437 return theCoding.getCode(); 438 } 439 440 private static String extractCodingVersion( 441 FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) { 442 if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 443 return null; 444 } 445 return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version"); 446 } 447 448 public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) { 449 String codeSystemUrl = theCodeSystemUrl; 450 if (isNotBlank(theVersion)) { 451 codeSystemUrl = codeSystemUrl + "|" + theVersion; 452 } 453 return codeSystemUrl; 454 } 455}