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}