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