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