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