
001/* 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 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.support.IValidationSupport; 023import ca.uhn.fhir.context.support.TranslateConceptResults; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoConceptMap; 026import ca.uhn.fhir.jpa.api.model.TranslationRequest; 027import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 028import ca.uhn.fhir.jpa.model.entity.ResourceTable; 029import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 030import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; 031import ca.uhn.fhir.rest.api.server.IBundleProvider; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 034import ca.uhn.fhir.rest.param.TokenParam; 035import ca.uhn.fhir.rest.param.UriParam; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 037import ca.uhn.fhir.util.OperationOutcomeUtil; 038import ca.uhn.fhir.util.ValidateUtil; 039import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 040import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042import org.hl7.fhir.r4.model.ConceptMap; 043import org.hl7.fhir.r4.model.Enumerations; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046import org.springframework.beans.factory.annotation.Autowired; 047 048import java.util.Date; 049import java.util.List; 050import java.util.stream.Collectors; 051 052import static org.apache.commons.lang3.StringUtils.isBlank; 053import static org.apache.commons.lang3.StringUtils.isNotBlank; 054 055public class JpaResourceDaoConceptMap<T extends IBaseResource> extends JpaResourceDao<T> 056 implements IFhirResourceDaoConceptMap<T> { 057 058 private static final Logger ourLog = LoggerFactory.getLogger(JpaResourceDaoConceptMap.class); 059 060 @Autowired 061 private ITermConceptMappingSvc myTermConceptMappingSvc; 062 063 @Autowired 064 private IValidationSupport myValidationSupport; 065 066 @Autowired 067 private VersionCanonicalizer myVersionCanonicalizer; 068 069 /** 070 * Operation: <code>ConceptMap/$translate</code> 071 */ 072 @Override 073 public TranslateConceptResults translate( 074 TranslationRequest theTranslationRequest, RequestDetails theRequestDetails) { 075 IValidationSupport.TranslateCodeRequest translateCodeRequest = theTranslationRequest.asTranslateCodeRequest(); 076 return myValidationSupport.translateConcept(translateCodeRequest); 077 } 078 079 @Override 080 public ResourceTable updateEntity( 081 RequestDetails theRequestDetails, 082 IBaseResource theResource, 083 IBasePersistedResource theEntity, 084 Date theDeletedTimestampOrNull, 085 boolean thePerformIndexing, 086 boolean theUpdateVersion, 087 TransactionDetails theTransactionDetails, 088 boolean theForceUpdate, 089 boolean theCreateNewHistoryEntry) { 090 ResourceTable retVal = super.updateEntity( 091 theRequestDetails, 092 theResource, 093 theEntity, 094 theDeletedTimestampOrNull, 095 thePerformIndexing, 096 theUpdateVersion, 097 theTransactionDetails, 098 theForceUpdate, 099 theCreateNewHistoryEntry); 100 101 boolean entityWasSaved = !retVal.isUnchangedInCurrentOperation(); 102 boolean shouldProcessUpdate = entityWasSaved && thePerformIndexing; 103 if (shouldProcessUpdate) { 104 if (retVal.getDeleted() == null) { 105 ConceptMap conceptMap = myVersionCanonicalizer.conceptMapToCanonical(theResource); 106 myTermConceptMappingSvc.storeTermConceptMapAndChildren(retVal, conceptMap); 107 } else { 108 myTermConceptMappingSvc.deleteConceptMapAndChildren(retVal); 109 } 110 } 111 112 return retVal; 113 } 114 115 /** 116 * Operation: <code>ConceptMap/$hapi.fhir.add-mapping</code> 117 */ 118 @SuppressWarnings("unchecked") 119 @Override 120 public IBaseOperationOutcome addMapping(AddMappingRequest theRequest, RequestDetails theRequestDetails) { 121 String sourceDisplay = theRequest.getSourceDisplay(); 122 String targetDisplay = theRequest.getTargetDisplay(); 123 String equivalence = theRequest.getEquivalence(); 124 ValidateUtil.isNotBlankOrThrowInvalidRequest(equivalence, "Equivalence must be provided"); 125 126 ConceptMap conceptMapCanonical = 127 fetchExistingConceptMapAndConvertToCanonical(theRequest, true, theRequestDetails); 128 129 List<ConceptMap.ConceptMapGroupComponent> groups = findOrCreateGroup(theRequest, conceptMapCanonical, true); 130 ConceptMap.ConceptMapGroupComponent group = groups.get(0); 131 132 List<ConceptMap.SourceElementComponent> sourceElements = 133 findOrCreateSourceElements(theRequest, group, true, sourceDisplay); 134 ConceptMap.SourceElementComponent sourceElement = sourceElements.get(0); 135 136 findOrCreateTargetElement(theRequest, sourceElement, true, targetDisplay, equivalence); 137 138 T conceptMapToStore = (T) myVersionCanonicalizer.conceptMapFromCanonical(conceptMapCanonical); 139 if (conceptMapToStore.getIdElement().hasIdPart()) { 140 update(conceptMapToStore, theRequestDetails); 141 } else { 142 create(conceptMapToStore, theRequestDetails); 143 } 144 145 IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.createOperationOutcome( 146 OperationOutcomeUtil.OO_SEVERITY_WARN, 147 "Mapping has been added", 148 OperationOutcomeUtil.OO_ISSUE_CODE_PROCESSING, 149 myFhirContext, 150 null); 151 return operationOutcome; 152 } 153 154 /** 155 * Operation: <code>ConceptMap/$hapi.fhir.remove-mapping</code> 156 */ 157 @SuppressWarnings("unchecked") 158 @Override 159 public IBaseOperationOutcome removeMapping(RemoveMappingRequest theRequest, RequestDetails theRequestDetails) { 160 ConceptMap conceptMapCanonical = 161 fetchExistingConceptMapAndConvertToCanonical(theRequest, false, theRequestDetails); 162 if (conceptMapCanonical == null) { 163 String message = "No ConceptMap found matching the given URL and/or version. No action performed."; 164 ourLog.warn(message); 165 IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.createOperationOutcome( 166 OperationOutcomeUtil.OO_SEVERITY_WARN, 167 message, 168 OperationOutcomeUtil.OO_ISSUE_CODE_PROCESSING, 169 myFhirContext, 170 null); 171 return operationOutcome; 172 } 173 174 int mappingsRemoved = 0; 175 List<ConceptMap.ConceptMapGroupComponent> groups = findOrCreateGroup(theRequest, conceptMapCanonical, false); 176 for (ConceptMap.ConceptMapGroupComponent group : groups) { 177 178 List<ConceptMap.SourceElementComponent> sourceElements = 179 findOrCreateSourceElements(theRequest, group, false, null); 180 for (ConceptMap.SourceElementComponent sourceElement : sourceElements) { 181 182 List<ConceptMap.TargetElementComponent> targetElements = 183 findOrCreateTargetElement(theRequest, sourceElement, false, null, null); 184 for (ConceptMap.TargetElementComponent targetElement : targetElements) { 185 mappingsRemoved++; 186 sourceElement.getTarget().remove(targetElement); 187 } 188 189 if (sourceElement.getTarget().isEmpty()) { 190 group.getElement().remove(sourceElement); 191 } 192 } 193 } 194 195 T conceptMapToStore = (T) myVersionCanonicalizer.conceptMapFromCanonical(conceptMapCanonical); 196 update(conceptMapToStore, theRequestDetails); 197 198 String message = "Removed " + mappingsRemoved + " ConceptMap mappings"; 199 ourLog.info(message); 200 IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.createOperationOutcome( 201 OperationOutcomeUtil.OO_SEVERITY_WARN, 202 message, 203 OperationOutcomeUtil.OO_ISSUE_CODE_PROCESSING, 204 myFhirContext, 205 null); 206 return operationOutcome; 207 } 208 209 private static List<ConceptMap.TargetElementComponent> findOrCreateTargetElement( 210 RemoveMappingRequest theRequest, 211 ConceptMap.SourceElementComponent sourceElement, 212 boolean theCreate, 213 String targetDisplay, 214 String equivalence) { 215 String targetCode = theRequest.getTargetCode(); 216 ValidateUtil.isNotBlankOrThrowInvalidRequest(targetCode, "Target code must be provided"); 217 218 List<ConceptMap.TargetElementComponent> retVal = sourceElement.getTarget().stream() 219 .filter(t -> targetCode.equals(t.getCode())) 220 .collect(Collectors.toList()); 221 222 if (retVal.isEmpty() && theCreate) { 223 ConceptMap.TargetElementComponent newTarget = sourceElement.addTarget(); 224 newTarget.setCode(targetCode); 225 newTarget.setDisplay(targetDisplay); 226 newTarget.setEquivalence(Enumerations.ConceptMapEquivalence.fromCode(equivalence)); 227 retVal.add(newTarget); 228 } 229 230 return retVal; 231 } 232 233 private static List<ConceptMap.SourceElementComponent> findOrCreateSourceElements( 234 RemoveMappingRequest theRequest, 235 ConceptMap.ConceptMapGroupComponent group, 236 boolean theCreate, 237 String sourceDisplay) { 238 String sourceCode = theRequest.getSourceCode(); 239 ValidateUtil.isNotBlankOrThrowInvalidRequest(sourceCode, "Source code must be provided"); 240 241 List<ConceptMap.SourceElementComponent> retVal = group.getElement().stream() 242 .filter(t -> sourceCode.equals(t.getCode())) 243 .collect(Collectors.toList()); 244 245 if (retVal.isEmpty() && theCreate) { 246 ConceptMap.SourceElementComponent newElement = group.addElement(); 247 newElement.setCode(sourceCode); 248 newElement.setDisplay(sourceDisplay); 249 retVal.add(newElement); 250 } 251 252 return retVal; 253 } 254 255 private static List<ConceptMap.ConceptMapGroupComponent> findOrCreateGroup( 256 RemoveMappingRequest theRequest, ConceptMap conceptMapCanonical, boolean theCreateIfNotFound) { 257 String sourceSystem = theRequest.getSourceSystem(); 258 ValidateUtil.isNotBlankOrThrowInvalidRequest(sourceSystem, "Source system must be provided"); 259 String sourceSystemVersion = theRequest.getSourceSystemVersion(); 260 String targetSystem = theRequest.getTargetSystem(); 261 ValidateUtil.isNotBlankOrThrowInvalidRequest(sourceSystem, "Target system must be provided"); 262 String targetSystemVersion = theRequest.getTargetSystemVersion(); 263 264 List<ConceptMap.ConceptMapGroupComponent> retVal = conceptMapCanonical.getGroup().stream() 265 .filter(t -> { 266 boolean match = sourceSystem.equals(t.getSource()); 267 match &= targetSystem.equals(t.getTarget()); 268 match &= isBlank(sourceSystemVersion) || sourceSystemVersion.equals(t.getSourceVersion()); 269 match &= isBlank(targetSystemVersion) || targetSystemVersion.equals(t.getTargetVersion()); 270 return match; 271 }) 272 .collect(Collectors.toList()); 273 274 if (retVal.isEmpty() && theCreateIfNotFound) { 275 ConceptMap.ConceptMapGroupComponent newGroup = conceptMapCanonical.addGroup(); 276 newGroup.setSource(sourceSystem); 277 newGroup.setSourceVersion(sourceSystemVersion); 278 newGroup.setTarget(targetSystem); 279 newGroup.setTargetVersion(targetSystemVersion); 280 retVal.add(newGroup); 281 } 282 283 return retVal; 284 } 285 286 private ConceptMap fetchExistingConceptMapAndConvertToCanonical( 287 RemoveMappingRequest theRequest, boolean theCreateIfNotFound, RequestDetails theRequestDetails) { 288 String conceptMapUrl = theRequest.getConceptMapUri(); 289 ValidateUtil.isNotBlankOrThrowInvalidRequest(conceptMapUrl, "ConceptMap URI must be provided"); 290 String conceptMapVersion = theRequest.getConceptMapVersion(); 291 292 SearchParameterMap map = conceptMapUrlToParameterMap(conceptMapUrl, conceptMapVersion); 293 IBundleProvider bundle = search(map, theRequestDetails); 294 295 ConceptMap conceptMapCanonical; 296 if (bundle.sizeOrThrowNpe() > 1) { 297 throw new InvalidRequestException(Msg.code(1743) + "Multiple ConceptMap resources match URL[" 298 + conceptMapUrl + "]. Do you need to specify a version?"); 299 } else if (bundle.isEmpty()) { 300 301 if (theCreateIfNotFound) { 302 ourLog.info("Creating new ConceptMap with URL: {}", conceptMapUrl); 303 conceptMapCanonical = new ConceptMap(); 304 conceptMapCanonical.setUrl(conceptMapUrl); 305 conceptMapCanonical.setDate(new Date()); 306 conceptMapCanonical.setDescription("Automatically created by HAPI FHIR"); 307 } else { 308 conceptMapCanonical = null; 309 } 310 311 } else { 312 313 IBaseResource conceptMap = bundle.getResources(0, 1).get(0); 314 conceptMapCanonical = myVersionCanonicalizer.conceptMapToCanonical(conceptMap); 315 } 316 return conceptMapCanonical; 317 } 318 319 /** 320 * @param theConceptMapUrl The URL. Can include a <code>|version</code> suffix, in which case 321 * {@literal theConceptMapVersion} should be null. 322 * @param theConceptMapVersion The version 323 */ 324 public static SearchParameterMap conceptMapUrlToParameterMap(String theConceptMapUrl, String theConceptMapVersion) { 325 326 String url; 327 String version; 328 329 int pipeIndex = theConceptMapUrl.indexOf('|'); 330 if (pipeIndex > 0) { 331 url = theConceptMapUrl.substring(0, pipeIndex); 332 version = theConceptMapUrl.substring(pipeIndex + 1); 333 } else { 334 url = theConceptMapUrl; 335 version = theConceptMapVersion; 336 } 337 338 if (isNotBlank(theConceptMapVersion) && isNotBlank(version) && !theConceptMapVersion.equals(version)) { 339 throw new InvalidRequestException(Msg.code(2818) + "ConceptMap URL includes a version[" + version 340 + "] which conflicts with specified version[" + theConceptMapVersion + "]"); 341 } 342 343 SearchParameterMap map = new SearchParameterMap(); 344 map.setLoadSynchronousUpTo(2); 345 map.add(ConceptMap.SP_URL, new UriParam(url)); 346 if (version != null) { 347 map.add(ConceptMap.SP_VERSION, new TokenParam(version)); 348 } 349 return map; 350 } 351}