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