001/*- 002 * #%L 003 * HAPI FHIR JPA Server - Master Data Management 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.mdm.broker; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.mdm.svc.IMdmModelConverterSvc; 028import ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc; 029import ca.uhn.fhir.jpa.mdm.svc.MdmResourceFilteringSvc; 030import ca.uhn.fhir.jpa.mdm.svc.candidate.TooManyCandidatesException; 031import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; 032import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; 033import ca.uhn.fhir.mdm.api.IMdmSettings; 034import ca.uhn.fhir.mdm.api.MdmLinkEvent; 035import ca.uhn.fhir.mdm.log.Logs; 036import ca.uhn.fhir.mdm.model.MdmTransactionContext; 037import ca.uhn.fhir.rest.api.Constants; 038import ca.uhn.fhir.rest.server.TransactionLogMessages; 039import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 040import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage; 041import org.hl7.fhir.instance.model.api.IAnyResource; 042import org.hl7.fhir.instance.model.api.IBaseResource; 043import org.slf4j.Logger; 044import org.springframework.beans.factory.annotation.Autowired; 045import org.springframework.messaging.Message; 046import org.springframework.messaging.MessageHandler; 047import org.springframework.messaging.MessagingException; 048import org.springframework.stereotype.Service; 049 050@Service 051public class MdmMessageHandler implements MessageHandler { 052 053 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 054 055 @Autowired 056 private MdmMatchLinkSvc myMdmMatchLinkSvc; 057 058 @Autowired 059 private IInterceptorBroadcaster myInterceptorBroadcaster; 060 061 @Autowired 062 private FhirContext myFhirContext; 063 064 @Autowired 065 private MdmResourceFilteringSvc myMdmResourceFilteringSvc; 066 067 @Autowired 068 private IMdmSettings myMdmSettings; 069 070 @Autowired 071 private IMdmModelConverterSvc myModelConverter; 072 073 @Override 074 public void handleMessage(Message<?> theMessage) throws MessagingException { 075 ourLog.trace("Handling resource modified message: {}", theMessage); 076 077 if (!(theMessage instanceof ResourceModifiedJsonMessage)) { 078 ourLog.warn("Unexpected message payload type: {}", theMessage); 079 return; 080 } 081 082 ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload(); 083 try { 084 085 IBaseResource sourceResource = msg.getNewPayload(myFhirContext); 086 087 if (myMdmResourceFilteringSvc.shouldBeProcessed((IAnyResource) sourceResource)) { 088 matchMdmAndUpdateLinks(sourceResource, msg); 089 } 090 } catch (TooManyCandidatesException e) { 091 ourLog.error(e.getMessage(), e); 092 // skip this one with an error message and continue processing 093 } catch (Exception e) { 094 ourLog.error("Failed to handle MDM Matching Resource:", e); 095 throw e; 096 } 097 } 098 099 private void matchMdmAndUpdateLinks(IBaseResource theSourceResource, ResourceModifiedMessage theMsg) { 100 101 String resourceType = theSourceResource.getIdElement().getResourceType(); 102 validateResourceType(resourceType); 103 104 if (myInterceptorBroadcaster.hasHooks(Pointcut.MDM_BEFORE_PERSISTED_RESOURCE_CHECKED)) { 105 HookParams params = new HookParams().add(IBaseResource.class, theSourceResource); 106 myInterceptorBroadcaster.callHooks(Pointcut.MDM_BEFORE_PERSISTED_RESOURCE_CHECKED, params); 107 } 108 109 theSourceResource.setUserData(Constants.RESOURCE_PARTITION_ID, theMsg.getPartitionId()); 110 111 MdmTransactionContext mdmContext = createMdmContext(theMsg, resourceType); 112 try { 113 switch (theMsg.getOperationType()) { 114 case CREATE: 115 handleCreateResource(theSourceResource, mdmContext); 116 break; 117 case UPDATE: 118 case MANUALLY_TRIGGERED: 119 handleUpdateResource(theSourceResource, mdmContext); 120 break; 121 case DELETE: 122 default: 123 ourLog.trace("Not processing modified message for {}", theMsg.getOperationType()); 124 } 125 } catch (Exception e) { 126 log(mdmContext, "Failure during MDM processing: " + e.getMessage(), e); 127 mdmContext.addTransactionLogMessage(e.getMessage()); 128 } finally { 129 // Interceptor call: MDM_AFTER_PERSISTED_RESOURCE_CHECKED 130 HookParams params = new HookParams() 131 .add(ResourceOperationMessage.class, getOutgoingMessage(theMsg)) 132 .add(TransactionLogMessages.class, mdmContext.getTransactionLogMessages()) 133 .add(MdmLinkEvent.class, buildLinkChangeEvent(mdmContext)); 134 135 myInterceptorBroadcaster.callHooks(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, params); 136 } 137 } 138 139 private MdmTransactionContext createMdmContext(ResourceModifiedMessage theMsg, String theResourceType) { 140 TransactionLogMessages transactionLogMessages = 141 TransactionLogMessages.createFromTransactionGuid(theMsg.getTransactionId()); 142 MdmTransactionContext.OperationType mdmOperation; 143 switch (theMsg.getOperationType()) { 144 case CREATE: 145 mdmOperation = MdmTransactionContext.OperationType.CREATE_RESOURCE; 146 break; 147 case UPDATE: 148 mdmOperation = MdmTransactionContext.OperationType.UPDATE_RESOURCE; 149 break; 150 case MANUALLY_TRIGGERED: 151 mdmOperation = MdmTransactionContext.OperationType.SUBMIT_RESOURCE_TO_MDM; 152 break; 153 case DELETE: 154 default: 155 ourLog.trace("Not creating an MdmTransactionContext for {}", theMsg.getOperationType()); 156 throw new InvalidRequestException( 157 Msg.code(734) + "We can't handle non-update/create operations in MDM"); 158 } 159 return new MdmTransactionContext(transactionLogMessages, mdmOperation, theResourceType); 160 } 161 162 private void validateResourceType(String theResourceType) { 163 if (!myMdmSettings.isSupportedMdmType(theResourceType)) { 164 throw new IllegalStateException( 165 Msg.code(735) + "Unsupported resource type submitted to MDM matching queue: " + theResourceType); 166 } 167 } 168 169 private void handleCreateResource(IBaseResource theResource, MdmTransactionContext theMdmTransactionContext) { 170 myMdmMatchLinkSvc.updateMdmLinksForMdmSource((IAnyResource) theResource, theMdmTransactionContext); 171 } 172 173 private void handleUpdateResource(IBaseResource theResource, MdmTransactionContext theMdmTransactionContext) { 174 myMdmMatchLinkSvc.updateMdmLinksForMdmSource((IAnyResource) theResource, theMdmTransactionContext); 175 } 176 177 private void log(MdmTransactionContext theMdmContext, String theMessage, Exception theException) { 178 theMdmContext.addTransactionLogMessage(theMessage); 179 ourLog.error(theMessage, theException); 180 } 181 182 private MdmLinkEvent buildLinkChangeEvent(MdmTransactionContext theMdmContext) { 183 MdmLinkEvent linkChangeEvent = new MdmLinkEvent(); 184 theMdmContext.getMdmLinks().stream().forEach(l -> { 185 linkChangeEvent.addMdmLink(myModelConverter.toJson(l)); 186 }); 187 188 return linkChangeEvent; 189 } 190 191 private ResourceOperationMessage getOutgoingMessage(ResourceModifiedMessage theMsg) { 192 IBaseResource targetResource = theMsg.getPayload(myFhirContext); 193 ResourceOperationMessage outgoingMsg = 194 new ResourceOperationMessage(myFhirContext, targetResource, theMsg.getOperationType()); 195 outgoingMsg.setTransactionId(theMsg.getTransactionId()); 196 197 return outgoingMsg; 198 } 199}