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