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.dao.IFhirResourceDaoCodeSystem; 031import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 032import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 033import ca.uhn.fhir.jpa.model.entity.ResourceTable; 034import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 035import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; 036import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; 037import ca.uhn.fhir.jpa.util.LogicUtil; 038import ca.uhn.fhir.rest.api.server.RequestDetails; 039import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 040import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 041import ca.uhn.fhir.rest.param.TokenParam; 042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 043import ca.uhn.fhir.util.FhirTerser; 044import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 045import jakarta.annotation.Nonnull; 046import jakarta.annotation.PostConstruct; 047import org.apache.commons.collections4.CollectionUtils; 048import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; 049import org.hl7.fhir.instance.model.api.IBaseCoding; 050import org.hl7.fhir.instance.model.api.IBaseDatatype; 051import org.hl7.fhir.instance.model.api.IBaseResource; 052import org.hl7.fhir.instance.model.api.IIdType; 053import org.hl7.fhir.instance.model.api.IPrimitiveType; 054import org.hl7.fhir.r4.model.CodeableConcept; 055import org.hl7.fhir.r4.model.Coding; 056import org.springframework.beans.factory.annotation.Autowired; 057 058import java.util.ArrayList; 059import java.util.Collection; 060import java.util.Date; 061import java.util.List; 062import java.util.stream.Collectors; 063 064import static ca.uhn.fhir.util.DatatypeUtil.toStringValue; 065import static org.apache.commons.lang3.StringUtils.isNotBlank; 066 067public class JpaResourceDaoCodeSystem<T extends IBaseResource> extends BaseHapiFhirResourceDao<T> 068 implements IFhirResourceDaoCodeSystem<T> { 069 070 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JpaResourceDaoCodeSystem.class); 071 072 @Autowired 073 protected ITermCodeSystemStorageSvc myTerminologyCodeSystemStorageSvc; 074 075 @Autowired 076 protected IIdHelperService myIdHelperService; 077 078 @Autowired 079 protected ITermDeferredStorageSvc myTermDeferredStorageSvc; 080 081 @Autowired 082 private IValidationSupport myValidationSupport; 083 084 @Autowired 085 private FhirContext myFhirContext; 086 087 private FhirTerser myTerser; 088 089 @Autowired 090 private VersionCanonicalizer myVersionCanonicalizer; 091 092 @Override 093 @PostConstruct 094 public void start() { 095 super.start(); 096 myTerser = myFhirContext.newTerser(); 097 } 098 099 @Override 100 public List<IIdType> findCodeSystemIdsContainingSystemAndCode( 101 String theCode, String theSystem, RequestDetails theRequest) { 102 List<IIdType> valueSetIds; 103 List<IResourcePersistentId> ids = searchForIds( 104 new SearchParameterMap(org.hl7.fhir.r4.model.CodeSystem.SP_CODE, new TokenParam(theSystem, theCode)), 105 theRequest); 106 valueSetIds = new ArrayList<>(); 107 for (IResourcePersistentId next : ids) { 108 IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "CodeSystem", next); 109 valueSetIds.add(id); 110 } 111 return valueSetIds; 112 } 113 114 @Nonnull 115 @Override 116 public IValidationSupport.LookupCodeResult lookupCode( 117 IPrimitiveType<String> theCode, 118 IPrimitiveType<String> theSystem, 119 IBaseCoding theCoding, 120 RequestDetails theRequestDetails) { 121 return lookupCode(theCode, theSystem, theCoding, null, theRequestDetails); 122 } 123 124 @Nonnull 125 @Override 126 public IValidationSupport.LookupCodeResult lookupCode( 127 IPrimitiveType<String> theCode, 128 IPrimitiveType<String> theSystem, 129 IBaseCoding theCoding, 130 IPrimitiveType<String> theDisplayLanguage, 131 RequestDetails theRequestDetails) { 132 return lookupCode( 133 theCode, 134 theSystem, 135 theCoding, 136 theDisplayLanguage, 137 CollectionUtils.emptyCollection(), 138 theRequestDetails); 139 } 140 141 @Nonnull 142 @Override 143 public IValidationSupport.LookupCodeResult lookupCode( 144 IPrimitiveType<String> theCode, 145 IPrimitiveType<String> theSystem, 146 IBaseCoding theCoding, 147 IPrimitiveType<String> theDisplayLanguage, 148 Collection<IPrimitiveType<String>> thePropertyNames, 149 RequestDetails theRequestDetails) { 150 return doLookupCode( 151 myFhirContext, 152 myTerser, 153 myValidationSupport, 154 theCode, 155 theSystem, 156 theCoding, 157 theDisplayLanguage, 158 thePropertyNames); 159 } 160 161 @Override 162 public SubsumesResult subsumes( 163 IPrimitiveType<String> theCodeA, 164 IPrimitiveType<String> theCodeB, 165 IPrimitiveType<String> theSystem, 166 IBaseCoding theCodingA, 167 IBaseCoding theCodingB, 168 RequestDetails theRequestDetails) { 169 return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB); 170 } 171 172 @Override 173 protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) { 174 super.preDelete(theResourceToDelete, theEntityToDelete, theRequestDetails); 175 176 myTermDeferredStorageSvc.deleteCodeSystemForResource(theEntityToDelete); 177 } 178 179 @Override 180 public ResourceTable updateEntity( 181 RequestDetails theRequest, 182 IBaseResource theResource, 183 IBasePersistedResource theEntity, 184 Date theDeletedTimestampOrNull, 185 boolean thePerformIndexing, 186 boolean theUpdateVersion, 187 TransactionDetails theTransactionDetails, 188 boolean theForceUpdate, 189 boolean theCreateNewHistoryEntry) { 190 ResourceTable retVal = super.updateEntity( 191 theRequest, 192 theResource, 193 theEntity, 194 theDeletedTimestampOrNull, 195 thePerformIndexing, 196 theUpdateVersion, 197 theTransactionDetails, 198 theForceUpdate, 199 theCreateNewHistoryEntry); 200 if (!retVal.isUnchangedInCurrentOperation()) { 201 202 org.hl7.fhir.r4.model.CodeSystem cs = myVersionCanonicalizer.codeSystemToCanonical(theResource); 203 addPidToResource(theEntity, cs); 204 205 myTerminologyCodeSystemStorageSvc.storeNewCodeSystemVersionIfNeeded( 206 cs, (ResourceTable) theEntity, theRequest); 207 } 208 209 return retVal; 210 } 211 212 @Nonnull 213 @Override 214 public CodeValidationResult validateCode( 215 IIdType theCodeSystemId, 216 IPrimitiveType<String> theCodeSystemUrl, 217 IPrimitiveType<String> theVersion, 218 IPrimitiveType<String> theCode, 219 IPrimitiveType<String> theDisplay, 220 IBaseCoding theCoding, 221 IBaseDatatype theCodeableConcept, 222 RequestDetails theRequestDetails) { 223 224 CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept); 225 boolean haveCodeableConcept = 226 codeableConcept != null && codeableConcept.getCoding().size() > 0; 227 228 Coding coding = myVersionCanonicalizer.codingToCanonical(theCoding); 229 boolean haveCoding = coding != null && !coding.isEmpty(); 230 231 String code = toStringValue(theCode); 232 boolean haveCode = isNotBlank(code); 233 234 if (!haveCodeableConcept && !haveCoding && !haveCode) { 235 throw new InvalidRequestException( 236 Msg.code(906) + "No code, coding, or codeableConcept provided to validate."); 237 } 238 if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) { 239 throw new InvalidRequestException( 240 Msg.code(907) + "$validate-code can only validate (code) OR (coding) OR (codeableConcept)"); 241 } 242 243 String codeSystemUrl; 244 if (theCodeSystemId != null) { 245 IBaseResource codeSystem = read(theCodeSystemId, theRequestDetails); 246 codeSystemUrl = CommonCodeSystemsTerminologyService.getCodeSystemUrl(myFhirContext, codeSystem); 247 } else if (isNotBlank(toStringValue(theCodeSystemUrl))) { 248 codeSystemUrl = toStringValue(theCodeSystemUrl); 249 } else { 250 throw new InvalidRequestException(Msg.code(908) 251 + "Either CodeSystem ID or CodeSystem identifier must be provided. Unable to validate."); 252 } 253 254 if (haveCodeableConcept) { 255 CodeValidationResult anyValidation = null; 256 for (int i = 0; i < codeableConcept.getCoding().size(); i++) { 257 Coding nextCoding = codeableConcept.getCoding().get(i); 258 if (nextCoding.hasSystem()) { 259 if (!codeSystemUrl.equalsIgnoreCase(nextCoding.getSystem())) { 260 throw new InvalidRequestException(Msg.code(909) + "Coding.system '" + nextCoding.getSystem() 261 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 262 } 263 codeSystemUrl = nextCoding.getSystem(); 264 } 265 code = nextCoding.getCode(); 266 String display = nextCoding.getDisplay(); 267 CodeValidationResult nextValidation = 268 codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 269 anyValidation = nextValidation; 270 if (nextValidation.isOk()) { 271 return nextValidation; 272 } 273 } 274 return anyValidation; 275 } else if (haveCoding) { 276 if (coding.hasSystem()) { 277 if (!codeSystemUrl.equalsIgnoreCase(coding.getSystem())) { 278 throw new InvalidRequestException(Msg.code(910) + "Coding.system '" + coding.getSystem() 279 + "' does not equal with CodeSystem.url '" + codeSystemUrl + "'. Unable to validate."); 280 } 281 codeSystemUrl = coding.getSystem(); 282 } 283 code = coding.getCode(); 284 String display = coding.getDisplay(); 285 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 286 } else { 287 String display = toStringValue(theDisplay); 288 return codeSystemValidateCode(codeSystemUrl, toStringValue(theVersion), code, display); 289 } 290 } 291 292 private CodeValidationResult codeSystemValidateCode( 293 String theCodeSystemUrl, String theVersion, String theCode, String theDisplay) { 294 ValidationSupportContext context = new ValidationSupportContext(myValidationSupport); 295 ConceptValidationOptions options = new ConceptValidationOptions(); 296 options.setValidateDisplay(isNotBlank(theDisplay)); 297 298 String codeSystemUrl = createVersionedSystemIfVersionIsPresent(theCodeSystemUrl, theVersion); 299 300 CodeValidationResult retVal = 301 myValidationSupport.validateCode(context, options, codeSystemUrl, theCode, theDisplay, null); 302 if (retVal == null) { 303 retVal = new CodeValidationResult(); 304 retVal.setMessage( 305 "Terminology service was unable to provide validation for " + codeSystemUrl + "#" + theCode); 306 } 307 return retVal; 308 } 309 310 public static IValidationSupport.LookupCodeResult doLookupCode( 311 FhirContext theFhirContext, 312 FhirTerser theFhirTerser, 313 IValidationSupport theValidationSupport, 314 IPrimitiveType<String> theCode, 315 IPrimitiveType<String> theSystem, 316 IBaseCoding theCoding, 317 IPrimitiveType<String> theDisplayLanguage, 318 Collection<IPrimitiveType<String>> thePropertyNames) { 319 boolean haveCoding = theCoding != null 320 && isNotBlank(extractCodingSystem(theCoding)) 321 && isNotBlank(extractCodingCode(theCoding)); 322 boolean haveCode = theCode != null && theCode.isEmpty() == false; 323 boolean haveSystem = theSystem != null && theSystem.isEmpty() == false; 324 boolean haveDisplayLanguage = theDisplayLanguage != null && theDisplayLanguage.isEmpty() == false; 325 326 if (!haveCoding && !(haveSystem && haveCode)) { 327 throw new InvalidRequestException( 328 Msg.code(1126) + "No code, coding, or codeableConcept provided to validate"); 329 } 330 if (!LogicUtil.multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) { 331 throw new InvalidRequestException( 332 Msg.code(1127) + "$lookup can only validate (system AND code) OR (coding.system AND coding.code)"); 333 } 334 335 String code; 336 String system; 337 if (haveCoding) { 338 code = extractCodingCode(theCoding); 339 system = extractCodingSystem(theCoding); 340 String version = extractCodingVersion(theFhirContext, theFhirTerser, theCoding); 341 if (isNotBlank(version)) { 342 system = system + "|" + version; 343 } 344 } else { 345 code = theCode.getValue(); 346 system = theSystem.getValue(); 347 } 348 349 String displayLanguage = null; 350 if (haveDisplayLanguage) { 351 displayLanguage = theDisplayLanguage.getValue(); 352 } 353 354 ourLog.info("Looking up {} / {}", system, code); 355 356 Collection<String> propertyNames = CollectionUtils.emptyIfNull(thePropertyNames).stream() 357 .map(IPrimitiveType::getValueAsString) 358 .collect(Collectors.toSet()); 359 360 if (theValidationSupport.isCodeSystemSupported(new ValidationSupportContext(theValidationSupport), system)) { 361 362 ourLog.info("Code system {} is supported", system); 363 IValidationSupport.LookupCodeResult retVal = theValidationSupport.lookupCode( 364 new ValidationSupportContext(theValidationSupport), 365 new LookupCodeRequest(system, code, displayLanguage, propertyNames)); 366 if (retVal != null) { 367 return retVal; 368 } 369 } 370 371 // We didn't find it.. 372 return IValidationSupport.LookupCodeResult.notFound(system, code); 373 } 374 375 private static String extractCodingSystem(IBaseCoding theCoding) { 376 return theCoding.getSystem(); 377 } 378 379 private static String extractCodingCode(IBaseCoding theCoding) { 380 return theCoding.getCode(); 381 } 382 383 private static String extractCodingVersion( 384 FhirContext theFhirContext, FhirTerser theFhirTerser, IBaseCoding theCoding) { 385 if (theFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 386 return null; 387 } 388 return theFhirTerser.getSinglePrimitiveValueOrNull(theCoding, "version"); 389 } 390 391 public static String createVersionedSystemIfVersionIsPresent(String theCodeSystemUrl, String theVersion) { 392 String codeSystemUrl = theCodeSystemUrl; 393 if (isNotBlank(theVersion)) { 394 codeSystemUrl = codeSystemUrl + "|" + theVersion; 395 } 396 return codeSystemUrl; 397 } 398}