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}