001package ca.uhn.fhir.jpa.mdm.svc;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server - Master Data Management
006 * %%
007 * Copyright (C) 2014 - 2023 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.i18n.Msg;
024import ca.uhn.fhir.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
026import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
027import ca.uhn.fhir.mdm.api.IMdmLink;
028import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
029import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
030import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
031import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
032import ca.uhn.fhir.mdm.log.Logs;
033import ca.uhn.fhir.mdm.model.MdmTransactionContext;
034import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
036import org.hl7.fhir.instance.model.api.IAnyResource;
037import org.slf4j.Logger;
038import org.springframework.beans.factory.annotation.Autowired;
039import org.springframework.stereotype.Service;
040import org.springframework.transaction.annotation.Transactional;
041
042import javax.annotation.Nonnull;
043import java.util.List;
044import java.util.Optional;
045
046/**
047 * This class is in charge of managing MdmLinks between Golden Resources and source resources
048 */
049@Service
050public class MdmLinkSvcImpl implements IMdmLinkSvc {
051
052        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
053
054        @Autowired
055        private MdmResourceDaoSvc myMdmResourceDaoSvc;
056        @Autowired
057        private MdmLinkDaoSvc myMdmLinkDaoSvc;
058        @Autowired
059        private IIdHelperService myIdHelperService;
060
061        @Override
062        @Transactional
063        public void updateLink(@Nonnull IAnyResource theGoldenResource, @Nonnull IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) {
064                if (theMatchOutcome.isPossibleDuplicate() && goldenResourceLinkedAsNoMatch(theGoldenResource, theSourceResource)) {
065                        log(theMdmTransactionContext, theGoldenResource.getIdElement().toUnqualifiedVersionless() +
066                                " is linked as NO_MATCH with " +
067                                theSourceResource.getIdElement().toUnqualifiedVersionless() +
068                                " not linking as POSSIBLE_DUPLICATE.");
069                        return;
070                }
071
072                MdmMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum();
073                validateRequestIsLegal(theGoldenResource, theSourceResource, matchResultEnum, theLinkSource);
074
075                myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType());
076                IMdmLink link = createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext);
077                theMdmTransactionContext.addMdmLink(link);
078        }
079
080        private boolean goldenResourceLinkedAsNoMatch(IAnyResource theGoldenResource, IAnyResource theSourceResource) {
081                IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
082                IResourcePersistentId sourceId = myIdHelperService.getPidOrThrowException(theSourceResource);
083                // TODO perf collapse into one query
084                return myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenResourceId, sourceId, MdmMatchResultEnum.NO_MATCH).isPresent() ||
085                        myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(sourceId, goldenResourceId, MdmMatchResultEnum.NO_MATCH).isPresent();
086        }
087
088        @Override
089        public void deleteLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmTransactionContext theMdmTransactionContext) {
090                if (theGoldenResource == null) {
091                        return;
092                }
093                Optional<? extends IMdmLink> optionalMdmLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theSourceResource);
094                if (optionalMdmLink.isPresent()) {
095                        IMdmLink mdmLink = optionalMdmLink.get();
096                        log(theMdmTransactionContext, "Deleting MdmLink [" + theGoldenResource.getIdElement().toVersionless() + " -> " + theSourceResource.getIdElement().toVersionless() + "] with result: " + mdmLink.getMatchResult());
097                        myMdmLinkDaoSvc.deleteLink(mdmLink);
098                        theMdmTransactionContext.addMdmLink(mdmLink);
099                }
100        }
101
102        @Override
103        @Transactional
104        public void deleteLinksWithAnyReferenceTo(List<IResourcePersistentId> theGoldenResourceIds) {
105                myMdmLinkDaoSvc.deleteLinksWithAnyReferenceToPids(theGoldenResourceIds);
106        }
107
108        /**
109         * Helper function which runs various business rules about what types of requests are allowed.
110         */
111        private void validateRequestIsLegal(IAnyResource theGoldenResource, IAnyResource theResource, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) {
112                Optional<? extends IMdmLink> oExistingLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theResource);
113                if (oExistingLink.isPresent() && systemIsAttemptingToModifyManualLink(theLinkSource, oExistingLink.get())) {
114                        throw new InternalErrorException(Msg.code(760) + "MDM system is not allowed to modify links on manually created links");
115                }
116
117                if (systemIsAttemptingToAddNoMatch(theLinkSource, theMatchResult)) {
118                        throw new InternalErrorException(Msg.code(761) + "MDM system is not allowed to automatically NO_MATCH a resource");
119                }
120        }
121
122        /**
123         * Helper function which detects when the MDM system is attempting to add a NO_MATCH link, which is not allowed.
124         */
125        private boolean systemIsAttemptingToAddNoMatch(MdmLinkSourceEnum theLinkSource, MdmMatchResultEnum theMatchResult) {
126                return theLinkSource == MdmLinkSourceEnum.AUTO && theMatchResult == MdmMatchResultEnum.NO_MATCH;
127        }
128
129        /**
130         * Helper function to let us catch when System MDM rules are attempting to override a manually defined link.
131         */
132        private boolean systemIsAttemptingToModifyManualLink(MdmLinkSourceEnum theIncomingSource, IMdmLink theExistingSource) {
133                return theIncomingSource == MdmLinkSourceEnum.AUTO && theExistingSource.isManual();
134        }
135
136        private Optional<? extends IMdmLink> getMdmLinkForGoldenResourceSourceResourcePair(@Nonnull IAnyResource theGoldenResource, @Nonnull IAnyResource theCandidate) {
137                if (theGoldenResource.getIdElement().getIdPart() == null || theCandidate.getIdElement().getIdPart() == null) {
138                        return Optional.empty();
139                } else {
140                        return myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(
141                                myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource),
142                                myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theCandidate)
143                        );
144                }
145        }
146
147        private IMdmLink createOrUpdateLinkEntity(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) {
148                return myMdmLinkDaoSvc.createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext);
149        }
150
151        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
152                theMdmTransactionContext.addTransactionLogMessage(theMessage);
153                ourLog.debug(theMessage);
154        }
155}