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.term; 021 022import ca.uhn.fhir.context.support.TranslateConceptResult; 023import ca.uhn.fhir.context.support.TranslateConceptResults; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.jpa.api.model.TranslationRequest; 026import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupDao; 027import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementDao; 028import ca.uhn.fhir.jpa.dao.data.ITermConceptMapGroupElementTargetDao; 029import ca.uhn.fhir.jpa.entity.TermConceptMap; 030import ca.uhn.fhir.jpa.entity.TermConceptMapGroup; 031import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement; 032import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget; 033import ca.uhn.fhir.jpa.model.entity.ResourceTable; 034import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 037import ca.uhn.fhir.util.ValidateUtil; 038import com.google.common.annotations.VisibleForTesting; 039import org.hl7.fhir.exceptions.FHIRException; 040import org.hl7.fhir.r4.model.BooleanType; 041import org.hl7.fhir.r4.model.CodeType; 042import org.hl7.fhir.r4.model.Coding; 043import org.hl7.fhir.r4.model.ConceptMap; 044import org.hl7.fhir.r4.model.Enumerations; 045import org.hl7.fhir.r4.model.IdType; 046import org.hl7.fhir.r4.model.Parameters; 047import org.hl7.fhir.r4.model.StringType; 048import org.hl7.fhir.r4.model.UriType; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051import org.springframework.beans.factory.annotation.Autowired; 052import org.springframework.transaction.annotation.Transactional; 053 054import java.util.Optional; 055 056import static ca.uhn.fhir.jpa.term.TermReadSvcImpl.isPlaceholder; 057import static org.apache.commons.lang3.StringUtils.isBlank; 058import static org.apache.commons.lang3.StringUtils.isNotBlank; 059 060public class TermConceptMappingSvcImpl extends TermConceptClientMappingSvcImpl implements ITermConceptMappingSvc { 061 062 private static final Logger ourLog = LoggerFactory.getLogger(TermConceptMappingSvcImpl.class); 063 064 @Autowired 065 protected ITermConceptMapGroupDao myConceptMapGroupDao; 066 067 @Autowired 068 protected ITermConceptMapGroupElementDao myConceptMapGroupElementDao; 069 070 @Autowired 071 protected ITermConceptMapGroupElementTargetDao myConceptMapGroupElementTargetDao; 072 073 @Override 074 public String getName() { 075 return getFhirContext().getVersion().getVersion() + " ConceptMap Validation Support"; 076 } 077 078 @Override 079 @Transactional 080 public void deleteConceptMapAndChildren(ResourceTable theResourceTable) { 081 deleteConceptMap(theResourceTable); 082 } 083 084 @Override 085 @Transactional 086 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 087 TranslationRequest request = TranslationRequest.fromTranslateCodeRequest(theRequest); 088 if (request.hasReverse() && request.getReverseAsBoolean()) { 089 return translateWithReverse(request); 090 } 091 092 return translate(request); 093 } 094 095 @Override 096 @Transactional 097 public void storeTermConceptMapAndChildren(ResourceTable theResourceTable, ConceptMap theConceptMap) { 098 099 ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied"); 100 if (isPlaceholder(theConceptMap)) { 101 ourLog.info( 102 "Not storing TermConceptMap for placeholder {}", 103 theConceptMap.getIdElement().toVersionless().getValueAsString()); 104 return; 105 } 106 107 ValidateUtil.isNotBlankOrThrowUnprocessableEntity( 108 theConceptMap.getUrl(), "ConceptMap has no value for ConceptMap.url"); 109 ourLog.info( 110 "Storing TermConceptMap for {}", 111 theConceptMap.getIdElement().toVersionless().getValueAsString()); 112 113 TermConceptMap termConceptMap = new TermConceptMap(); 114 termConceptMap.setResource(theResourceTable); 115 termConceptMap.setUrl(theConceptMap.getUrl()); 116 termConceptMap.setVersion(theConceptMap.getVersion()); 117 118 String source = theConceptMap.hasSourceUriType() 119 ? theConceptMap.getSourceUriType().getValueAsString() 120 : null; 121 String target = theConceptMap.hasTargetUriType() 122 ? theConceptMap.getTargetUriType().getValueAsString() 123 : null; 124 125 /* 126 * If this is a mapping between "resources" instead of purely between 127 * "concepts" (this is a weird concept that is technically possible, at least as of 128 * FHIR R4), don't try to store the mappings. 129 * 130 * See here for a description of what that is: 131 * http://hl7.org/fhir/conceptmap.html#bnr 132 */ 133 if ("StructureDefinition".equals(new IdType(source).getResourceType()) 134 || "StructureDefinition".equals(new IdType(target).getResourceType())) { 135 return; 136 } 137 138 if (source == null && theConceptMap.hasSourceCanonicalType()) { 139 source = theConceptMap.getSourceCanonicalType().getValueAsString(); 140 } 141 if (target == null && theConceptMap.hasTargetCanonicalType()) { 142 target = theConceptMap.getTargetCanonicalType().getValueAsString(); 143 } 144 145 /* 146 * For now we always delete old versions. At some point, it would be nice to allow configuration to keep old versions. 147 */ 148 deleteConceptMap(theResourceTable); 149 150 /* 151 * Do the upload. 152 */ 153 String conceptMapUrl = termConceptMap.getUrl(); 154 String conceptMapVersion = termConceptMap.getVersion(); 155 Optional<TermConceptMap> optionalExistingTermConceptMapByUrl; 156 if (isBlank(conceptMapVersion)) { 157 optionalExistingTermConceptMapByUrl = myConceptMapDao.findTermConceptMapByUrlAndNullVersion(conceptMapUrl); 158 } else { 159 optionalExistingTermConceptMapByUrl = 160 myConceptMapDao.findTermConceptMapByUrlAndVersion(conceptMapUrl, conceptMapVersion); 161 } 162 if (optionalExistingTermConceptMapByUrl.isEmpty()) { 163 try { 164 if (isNotBlank(source)) { 165 termConceptMap.setSource(source); 166 } 167 if (isNotBlank(target)) { 168 termConceptMap.setTarget(target); 169 } 170 } catch (FHIRException fe) { 171 throw new InternalErrorException(Msg.code(837) + fe); 172 } 173 termConceptMap = myConceptMapDao.save(termConceptMap); 174 int codesSaved = 0; 175 176 TermConceptMapGroup termConceptMapGroup; 177 for (ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) { 178 179 String groupSource = group.getSource(); 180 if (isBlank(groupSource)) { 181 groupSource = source; 182 } 183 if (isBlank(groupSource)) { 184 throw new UnprocessableEntityException(Msg.code(838) + "ConceptMap[url='" + theConceptMap.getUrl() 185 + "'] contains at least one group without a value in ConceptMap.group.source"); 186 } 187 188 String groupTarget = group.getTarget(); 189 if (isBlank(groupTarget)) { 190 groupTarget = target; 191 } 192 if (isBlank(groupTarget)) { 193 throw new UnprocessableEntityException(Msg.code(839) + "ConceptMap[url='" + theConceptMap.getUrl() 194 + "'] contains at least one group without a value in ConceptMap.group.target"); 195 } 196 197 termConceptMapGroup = new TermConceptMapGroup(); 198 termConceptMapGroup.setConceptMap(termConceptMap); 199 termConceptMapGroup.setSource(groupSource); 200 termConceptMapGroup.setSourceVersion(group.getSourceVersion()); 201 termConceptMapGroup.setTarget(groupTarget); 202 termConceptMapGroup.setTargetVersion(group.getTargetVersion()); 203 termConceptMap.getConceptMapGroups().add(termConceptMapGroup); 204 termConceptMapGroup = myConceptMapGroupDao.save(termConceptMapGroup); 205 206 if (group.hasElement()) { 207 TermConceptMapGroupElement termConceptMapGroupElement; 208 for (ConceptMap.SourceElementComponent element : group.getElement()) { 209 if (isBlank(element.getCode())) { 210 continue; 211 } 212 termConceptMapGroupElement = new TermConceptMapGroupElement(); 213 termConceptMapGroupElement.setConceptMapGroup(termConceptMapGroup); 214 termConceptMapGroupElement.setCode(element.getCode()); 215 termConceptMapGroupElement.setDisplay(element.getDisplay()); 216 termConceptMapGroup.getConceptMapGroupElements().add(termConceptMapGroupElement); 217 termConceptMapGroupElement = myConceptMapGroupElementDao.save(termConceptMapGroupElement); 218 219 if (element.hasTarget()) { 220 TermConceptMapGroupElementTarget termConceptMapGroupElementTarget; 221 for (ConceptMap.TargetElementComponent elementTarget : element.getTarget()) { 222 if (isBlank(elementTarget.getCode()) 223 && elementTarget.getEquivalence() 224 != Enumerations.ConceptMapEquivalence.UNMATCHED) { 225 continue; 226 } 227 termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget(); 228 termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement); 229 if (isNotBlank(elementTarget.getCode())) { 230 termConceptMapGroupElementTarget.setCode(elementTarget.getCode()); 231 termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay()); 232 } 233 termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence()); 234 termConceptMapGroupElement 235 .getConceptMapGroupElementTargets() 236 .add(termConceptMapGroupElementTarget); 237 myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget); 238 239 if (++codesSaved % 250 == 0) { 240 ourLog.info("Have saved {} codes in ConceptMap", codesSaved); 241 myConceptMapGroupElementTargetDao.flush(); 242 } 243 } 244 } 245 } 246 } 247 } 248 249 } else { 250 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get(); 251 252 if (isBlank(conceptMapVersion)) { 253 String msg = myContext 254 .getLocalizer() 255 .getMessage( 256 TermReadSvcImpl.class, 257 "cannotCreateDuplicateConceptMapUrl", 258 conceptMapUrl, 259 existingTermConceptMap 260 .getResource() 261 .getIdDt() 262 .toUnqualifiedVersionless() 263 .getValue()); 264 throw new UnprocessableEntityException(Msg.code(840) + msg); 265 266 } else { 267 String msg = myContext 268 .getLocalizer() 269 .getMessage( 270 TermReadSvcImpl.class, 271 "cannotCreateDuplicateConceptMapUrlAndVersion", 272 conceptMapUrl, 273 conceptMapVersion, 274 existingTermConceptMap 275 .getResource() 276 .getIdDt() 277 .toUnqualifiedVersionless() 278 .getValue()); 279 throw new UnprocessableEntityException(Msg.code(841) + msg); 280 } 281 } 282 283 ourLog.info( 284 "Done storing TermConceptMap[{}] for {}", 285 termConceptMap.getId(), 286 theConceptMap.getIdElement().toVersionless().getValueAsString()); 287 } 288 289 public void deleteConceptMap(ResourceTable theResourceTable) { 290 // Get existing entity so it can be deleted. 291 Optional<TermConceptMap> optionalExistingTermConceptMapById = 292 myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId()); 293 294 if (optionalExistingTermConceptMapById.isPresent()) { 295 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get(); 296 297 ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId()); 298 for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) { 299 300 for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) { 301 302 for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) { 303 304 myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId()); 305 } 306 307 myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId()); 308 } 309 310 myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId()); 311 } 312 313 myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId()); 314 ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId()); 315 } 316 } 317 318 /** 319 * This method is present only for unit tests, do not call from client code 320 */ 321 @VisibleForTesting 322 public static void clearOurLastResultsFromTranslationCache() { 323 ourLastResultsFromTranslationCache = false; 324 } 325 326 /** 327 * This method is present only for unit tests, do not call from client code 328 */ 329 @VisibleForTesting 330 public static void clearOurLastResultsFromTranslationWithReverseCache() { 331 ourLastResultsFromTranslationWithReverseCache = false; 332 } 333 334 /** 335 * This method is present only for unit tests, do not call from client code 336 */ 337 @VisibleForTesting 338 static boolean isOurLastResultsFromTranslationCache() { 339 return ourLastResultsFromTranslationCache; 340 } 341 342 /** 343 * This method is present only for unit tests, do not call from client code 344 */ 345 @VisibleForTesting 346 static boolean isOurLastResultsFromTranslationWithReverseCache() { 347 return ourLastResultsFromTranslationWithReverseCache; 348 } 349 350 public static Parameters toParameters(TranslateConceptResults theTranslationResult) { 351 Parameters retVal = new Parameters(); 352 353 retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult())); 354 355 if (theTranslationResult.getMessage() != null) { 356 retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage())); 357 } 358 359 for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) { 360 Parameters.ParametersParameterComponent matchParam = 361 retVal.addParameter().setName("match"); 362 populateTranslateMatchParts(translationMatch, matchParam); 363 } 364 365 return retVal; 366 } 367 368 private static void populateTranslateMatchParts( 369 TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) { 370 if (theTranslationMatch.getEquivalence() != null) { 371 theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence())); 372 } 373 374 if (isNotBlank(theTranslationMatch.getSystem()) 375 || isNotBlank(theTranslationMatch.getCode()) 376 || isNotBlank(theTranslationMatch.getDisplay())) { 377 Coding value = new Coding( 378 theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay()); 379 380 if (isNotBlank(theTranslationMatch.getSystemVersion())) { 381 value.setVersion(theTranslationMatch.getSystemVersion()); 382 } 383 384 theParam.addPart().setName("concept").setValue(value); 385 } 386 387 if (isNotBlank(theTranslationMatch.getConceptMapUrl())) { 388 theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl())); 389 } 390 } 391}