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.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 ResourceTable retVal = super.updateEntity( 236 theRequest, 237 theResource, 238 theEntity, 239 theDeletedTimestampOrNull, 240 thePerformIndexing, 241 theUpdateVersion, 242 theTransactionDetails, 243 theForceUpdate, 244 theCreateNewHistoryEntry); 245 if (thePerformIndexing) { 246 if (!retVal.isUnchangedInCurrentOperation()) { 247 248 org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource); 249 addPidToResource(theEntity, cs); 250 251 myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded( 252 cs, (ResourceTable) theEntity, theRequest); 253 } 254 } 255 256 return retVal; 257 } 258 259 @Nonnull 260 @Override 261 public CodeValidationResult validateCode( 262 IIdType theCodeSystemId, 263 IPrimitiveType<String> theCodeSystemUrl, 264 IPrimitiveType<String> theVersion, 265 IPrimitiveType<String> theCode, 266 IPrimitiveType<String> theDisplay, 267 IBaseCoding theCoding, 268 IBaseDatatype theCodeableConcept, 269 RequestDetails theRequestDetails) { 270 271 CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept); 272 boolean haveCodeableConcept = 273 codeableConcept != null && codeableConcept.getCoding().size() > 0; 274 275 Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding); 276 boolean haveCoding = coding != null && !coding.isEmpty(); 277 278 String code = toStringValue(theCode); 279 boolean haveCode = isNotBlank(code); 280 281 if (!haveCodeableConcept && !haveCoding && !haveCode) { 282 throw new InvalidRequestException( 283 Msg.code(906) + "No code, coding, or codeableConcept provided to validate."); 284 } 285 if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { 286 throw new InvalidRequestException( 287 Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)"); 288 } 289 290 String codeSystemUrl; 291 if (theCodeSystemId != null) { 292 IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails); 293 codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem); 294 } else if (isNotBlank(toStringValue(theCodeSystemUrl))) { 295 codeSystemUrl = toStringValue(theCodeSystemUrl); 296 } else { 297 throw new InvalidRequestException(Msg.code(908) 298 + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate."); 299 } 300 301 if (haveCodeableConcept) { 302 CodeValidationResult anyValidation = null; 303 for (int i = 0; i < codeableConcept.getCoding().size(); i++) { 304 Coding nextCoding = codeableConcept.getCoding().get(i); 305 if (nextCoding.hasSystem()) { 306 if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) { 307 throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem() 308 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 309 } 310 codeSystemUrl = nextCoding.getSystem(); 311 } 312 code = nextCoding.getCode(); 313 String display = nextCoding.getDisplay(); 314 CodeValidationResult nextValidation = 315 codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 316 anyValidation = nextValidation; 317 if (nextValidation.isOk()) { 318 return nextValidation; 319 } 320 } 321 return anyValidation; 322 } else if (haveCoding) { 323 if (coding.hasSystem()) { 324 if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) { 325 throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem() 326 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 327 } 328 codeSystemUrl = coding.getSystem(); 329 } 330 code = coding.getCode(); 331 String display = coding.getDisplay(); 332 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 333 } else { 334 String display = toStringValue(theDisplay); 335 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 336 } 337 } 338 339 private CodeValidationResult codeSystemValidateCode( 340 String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) { 341 ValidationSupportContext context = new ValidationSupportContext(myValidationSupport); 342 ConceptValidationOptions options = new ConceptValidationOptions(); 343 options.setValidateDisplay(isNotBlank(theDisplay)); 344 345 String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion); 346 347 CodeValidationResult retVal = 348 myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null); 349 if (retVal == null) { 350 retVal = new CodeValidationResult(); 351 retVal.setMessage( 352 "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode); 353 } 354 return retVal; 355 } 356 357 public static IValidationSupport.LookupCodeResult doLookupCode( 358 FhirContext theFhirContext, 359 FhirTerser theFhirTerser, 360 IValidationSupport theValidationSupport, 361 IPrimitiveType<String> theCode, 362 IPrimitiveType<String> theSystem, 363 IBaseCoding theCoding, 364 IPrimitiveType<String> theDisplayLanguage, 365 Collection<IPrimitiveType<String>> thePropertyNames) { 366 boolean haveCoding = theCoding != null 367 && isNotBlank(extractCodingSystem(theCoding)) 368 && isNotBlank(extractCodingCode(theCoding)); 369 boolean haveCode = theCode != null && theCode.isEmpty() == false; 370 boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; 371 boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false; 372 373 if (!haveCoding && !(haveSystem && haveCode)) { 374 throw new InvalidRequestException( 375 Msg.code(1126) + "No code, coding, or codeableConcept provided to validate"); 376 } 377 if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) { 378 throw new InvalidRequestException( 379 Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)"); 380 } 381 382 String code; 383 String system; 384 if (haveCoding) { 385 code = extractCodingCode(theCoding); 386 system = extractCodingSystem(theCoding); 387 String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding); 388 if (isNotBlank(version)) { 389 system = system + "|" + version; 390 } 391 } else { 392 code = theCode.getValue(); 393 system = theSystem.getValue(); 394 } 395 396 String displayLanguage = null; 397 if (haveDisplayLanguage) { 398 displayLanguage = theDisplayLanguage.getValue(); 399 } 400 401 ourLog.info("Looking up {} / {}", system, code); 402 403 Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream() 404 .map(IPrimitiveType::getValueAsString) 405 .collect(Collectors.toSet()); 406 407 if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) { 408 409 ourLog.info("Code system {} is supported", system); 410 IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode( 411 new ValidationSupportContext(theValidationSupport), 412 new LookupCodeRequest(system, code, displayLanguage, propertyNames)); 413 if (retVal != null) { 414 return retVal; 415 } 416 } 417 418 // We didn't find it.. 419 return IValidationSupport.LookupCodeResult.notFound(system, code); 420 } 421 422 private static String extractCodingSystem(IBaseCoding theCoding) { 423 return theCoding.getSystem(); 424 } 425 426 private static String extractCodingCode(IBaseCoding theCoding) { 427 return theCoding.getCode(); 428 } 429 430 private static String extractCodingVersion( 431 FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) { 432 if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 433 return null; 434 } 435 return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version"); 436 } 437 438 public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) { 439 String codeSystemUrl = theCodeSystemUrl; 440 if (isNotBlank(theVersion)) { 441 codeSystemUrl = codeSystemUrl + "|" + theVersion; 442 } 443 return codeSystemUrl; 444 } 445}