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 org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.r4.model.BooleanType; 040import org.hl7.fhir.r4.model.CodeType; 041import org.hl7.fhir.r4.model.Coding; 042import org.hl7.fhir.r4.model.ConceptMap; 043import org.hl7.fhir.r4.model.Enumerations; 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 && elementTarget.getEquivalence() 223 != Enumerations.ConceptMapEquivalence.UNMATCHED) { 224 continue; 225 } 226 termConceptMapGroupElementTarget = new TermConceptMapGroupElementTarget(); 227 termConceptMapGroupElementTarget.setConceptMapGroupElement(termConceptMapGroupElement); 228 if (isNotBlank(elementTarget.getCode())) { 229 termConceptMapGroupElementTarget.setCode(elementTarget.getCode()); 230 termConceptMapGroupElementTarget.setDisplay(elementTarget.getDisplay()); 231 } 232 termConceptMapGroupElementTarget.setEquivalence(elementTarget.getEquivalence()); 233 termConceptMapGroupElement 234 .getConceptMapGroupElementTargets() 235 .add(termConceptMapGroupElementTarget); 236 myConceptMapGroupElementTargetDao.save(termConceptMapGroupElementTarget); 237 238 if (++codesSaved % 250 == 0) { 239 ourLog.info("Have saved {} codes in ConceptMap", codesSaved); 240 myConceptMapGroupElementTargetDao.flush(); 241 } 242 } 243 } 244 } 245 } 246 } 247 248 } else { 249 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapByUrl.get(); 250 251 if (isBlank(conceptMapVersion)) { 252 String msg = myContext 253 .getLocalizer() 254 .getMessage( 255 TermReadSvcImpl.class, 256 "cannotCreateDuplicateConceptMapUrl", 257 conceptMapUrl, 258 existingTermConceptMap 259 .getResource() 260 .getIdDt() 261 .toUnqualifiedVersionless() 262 .getValue()); 263 throw new UnprocessableEntityException(Msg.code(840) + msg); 264 265 } else { 266 String msg = myContext 267 .getLocalizer() 268 .getMessage( 269 TermReadSvcImpl.class, 270 "cannotCreateDuplicateConceptMapUrlAndVersion", 271 conceptMapUrl, 272 conceptMapVersion, 273 existingTermConceptMap 274 .getResource() 275 .getIdDt() 276 .toUnqualifiedVersionless() 277 .getValue()); 278 throw new UnprocessableEntityException(Msg.code(841) + msg); 279 } 280 } 281 282 ourLog.info( 283 "Done storing TermConceptMap[{}] for {}", 284 termConceptMap.getId(), 285 theConceptMap.getIdElement().toVersionless().getValueAsString()); 286 } 287 288 public void deleteConceptMap(ResourceTable theResourceTable) { 289 // Get existing entity so it can be deleted. 290 Optional<TermConceptMap> optionalExistingTermConceptMapById = 291 myConceptMapDao.findTermConceptMapByResourcePid(theResourceTable.getId()); 292 293 if (optionalExistingTermConceptMapById.isPresent()) { 294 TermConceptMap existingTermConceptMap = optionalExistingTermConceptMapById.get(); 295 296 ourLog.info("Deleting existing TermConceptMap[{}] and its children...", existingTermConceptMap.getId()); 297 for (TermConceptMapGroup group : existingTermConceptMap.getConceptMapGroups()) { 298 299 for (TermConceptMapGroupElement element : group.getConceptMapGroupElements()) { 300 301 for (TermConceptMapGroupElementTarget target : element.getConceptMapGroupElementTargets()) { 302 303 myConceptMapGroupElementTargetDao.deleteTermConceptMapGroupElementTargetById(target.getId()); 304 } 305 306 myConceptMapGroupElementDao.deleteTermConceptMapGroupElementById(element.getId()); 307 } 308 309 myConceptMapGroupDao.deleteTermConceptMapGroupById(group.getId()); 310 } 311 312 myConceptMapDao.deleteTermConceptMapById(existingTermConceptMap.getId()); 313 ourLog.info("Done deleting existing TermConceptMap[{}] and its children.", existingTermConceptMap.getId()); 314 } 315 } 316 317 public static Parameters toParameters(TranslateConceptResults theTranslationResult) { 318 Parameters retVal = new Parameters(); 319 320 retVal.addParameter().setName("result").setValue(new BooleanType(theTranslationResult.getResult())); 321 322 if (theTranslationResult.getMessage() != null) { 323 retVal.addParameter().setName("message").setValue(new StringType(theTranslationResult.getMessage())); 324 } 325 326 for (TranslateConceptResult translationMatch : theTranslationResult.getResults()) { 327 Parameters.ParametersParameterComponent matchParam = 328 retVal.addParameter().setName("match"); 329 populateTranslateMatchParts(translationMatch, matchParam); 330 } 331 332 return retVal; 333 } 334 335 private static void populateTranslateMatchParts( 336 TranslateConceptResult theTranslationMatch, Parameters.ParametersParameterComponent theParam) { 337 if (theTranslationMatch.getEquivalence() != null) { 338 theParam.addPart().setName("equivalence").setValue(new CodeType(theTranslationMatch.getEquivalence())); 339 } 340 341 if (isNotBlank(theTranslationMatch.getSystem()) 342 || isNotBlank(theTranslationMatch.getCode()) 343 || isNotBlank(theTranslationMatch.getDisplay())) { 344 Coding value = new Coding( 345 theTranslationMatch.getSystem(), theTranslationMatch.getCode(), theTranslationMatch.getDisplay()); 346 347 if (isNotBlank(theTranslationMatch.getSystemVersion())) { 348 value.setVersion(theTranslationMatch.getSystemVersion()); 349 } 350 351 theParam.addPart().setName("concept").setValue(value); 352 } 353 354 if (isNotBlank(theTranslationMatch.getConceptMapUrl())) { 355 theParam.addPart().setName("source").setValue(new UriType(theTranslationMatch.getConceptMapUrl())); 356 } 357 } 358}