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.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
027import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
028import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper;
029import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
030import ca.uhn.fhir.mdm.api.IMdmLink;
031import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
032import ca.uhn.fhir.mdm.api.IMdmSettings;
033import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
034import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
035import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
036import ca.uhn.fhir.mdm.log.Logs;
037import ca.uhn.fhir.mdm.model.MdmTransactionContext;
038import ca.uhn.fhir.mdm.util.MdmResourceUtil;
039import ca.uhn.fhir.mdm.util.MessageHelper;
040import ca.uhn.fhir.rest.api.Constants;
041import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
042import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
043import ca.uhn.fhir.rest.server.provider.ProviderConstants;
044import org.hl7.fhir.instance.model.api.IAnyResource;
045import org.slf4j.Logger;
046import org.springframework.beans.factory.annotation.Autowired;
047import org.springframework.transaction.annotation.Transactional;
048
049import java.util.List;
050import java.util.Objects;
051import java.util.Optional;
052
053public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc {
054
055        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
056
057        @Autowired
058        FhirContext myFhirContext;
059        @Autowired
060        IIdHelperService myIdHelperService;
061        @Autowired
062        MdmLinkDaoSvc myMdmLinkDaoSvc;
063        @Autowired
064        MdmResourceDaoSvc myMdmResourceDaoSvc;
065        @Autowired
066        MdmMatchLinkSvc myMdmMatchLinkSvc;
067        @Autowired
068        IMdmSettings myMdmSettings;
069        @Autowired
070        MessageHelper myMessageHelper;
071        @Autowired
072        IMdmSurvivorshipService myMdmSurvivorshipService;
073        @Autowired
074        MdmPartitionHelper myMdmPartitionHelper;
075
076        @Transactional
077        @Override
078        public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) {
079                String sourceType = myFhirContext.getResourceType(theSourceResource);
080
081                validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType);
082
083                IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
084                IResourcePersistentId sourceResourceId = myIdHelperService.getPidOrThrowException(theSourceResource);
085
086                // check if the golden resource and the source resource are in the same partition, throw error if not
087                myMdmPartitionHelper.validateResourcesInSamePartition(theGoldenResource, theSourceResource);
088
089                Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, sourceResourceId);
090                if (optionalMdmLink.isEmpty()) {
091                        throw new InvalidRequestException(Msg.code(738) + myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource));
092                }
093
094                IMdmLink mdmLink = optionalMdmLink.get();
095
096                validateNoMatchPresentWhenAcceptingPossibleMatch(theSourceResource, goldenResourceId, theMatchResult);
097
098                if (mdmLink.getMatchResult() == theMatchResult) {
099                        ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ".  Nothing to do.");
100                        return theGoldenResource;
101                }
102
103                ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + ".");
104                mdmLink.setMatchResult(theMatchResult);
105                mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
106
107                // Add partition for the mdm link if it doesn't exist
108                RequestPartitionId goldenResourcePartitionId = (RequestPartitionId) theGoldenResource.getUserData(Constants.RESOURCE_PARTITION_ID);
109                if (goldenResourcePartitionId != null && goldenResourcePartitionId.hasPartitionIds() && goldenResourcePartitionId.getFirstPartitionIdOrNull() != null &&
110                        (mdmLink.getPartitionId() == null || mdmLink.getPartitionId().getPartitionId() == null)) {
111                        mdmLink.setPartitionId(new PartitionablePartitionId(goldenResourcePartitionId.getFirstPartitionIdOrNull(), goldenResourcePartitionId.getPartitionDate()));
112                }
113                myMdmLinkDaoSvc.save(mdmLink);
114
115                if (theMatchResult == MdmMatchResultEnum.MATCH) {
116                        // only apply survivorship rules in case of a match
117                        myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theSourceResource, theGoldenResource, theMdmContext);
118                }
119
120                myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType());
121                if (theMatchResult == MdmMatchResultEnum.NO_MATCH) {
122                        // Need to find a new Golden Resource to link this target to
123                        myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext);
124                }
125                return theGoldenResource;
126        }
127
128        /**
129         * When updating POSSIBLE_MATCH link to a MATCH we need to validate that a MATCH to a different golden resource
130         * doesn't exist, because a resource mustn't be a MATCH to more than one golden resource
131         */
132        private void validateNoMatchPresentWhenAcceptingPossibleMatch(IAnyResource theSourceResource,
133                                        IResourcePersistentId theGoldenResourceId, MdmMatchResultEnum theMatchResult) {
134
135                // if theMatchResult != MATCH, we are not accepting POSSIBLE_MATCH so there is nothing to validate
136                if (theMatchResult != MdmMatchResultEnum.MATCH) { return; }
137
138                IResourcePersistentId sourceResourceId = myIdHelperService.getPidOrThrowException(theSourceResource);
139                List<? extends IMdmLink> mdmLinks = myMdmLinkDaoSvc
140                        .getMdmLinksBySourcePidAndMatchResult(sourceResourceId, MdmMatchResultEnum.MATCH);
141
142                // if a link for a different golden resource exists, throw an exception
143                for (IMdmLink mdmLink : mdmLinks) {
144                        if (mdmLink.getGoldenResourcePersistenceId() != theGoldenResourceId) {
145                                IAnyResource existingGolden = myMdmResourceDaoSvc.readGoldenResourceByPid(mdmLink.getGoldenResourcePersistenceId(), mdmLink.getMdmSourceType());
146                                throw new InvalidRequestException(Msg.code(2218) +
147                                        myMessageHelper.getMessageForAlreadyAcceptedLink(existingGolden, theSourceResource));
148                        }
149                }
150        }
151
152
153        private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) {
154                String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord);
155
156                if (theMatchResult != MdmMatchResultEnum.NO_MATCH &&
157                        theMatchResult != MdmMatchResultEnum.MATCH) {
158                        throw new InvalidRequestException(Msg.code(739) + myMessageHelper.getMessageForUnsupportedMatchResult());
159                }
160
161                if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) {
162                        throw new InvalidRequestException(Msg.code(740) + myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType));
163                }
164
165                if (!myMdmSettings.isSupportedMdmType(theSourceType)) {
166                        throw new InvalidRequestException(Msg.code(741) + myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType));
167                }
168
169                if (!Objects.equals(goldenRecordType, theSourceType)) {
170                        throw new InvalidRequestException(Msg.code(742) + myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType));
171                }
172
173                if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) {
174                        throw new InvalidRequestException(Msg.code(743) + myMessageHelper.getMessageForUnmanagedResource());
175                }
176
177                if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) {
178                        throw new InvalidRequestException(Msg.code(744) + myMessageHelper.getMessageForUnsupportedSourceResource());
179                }
180        }
181
182        @Transactional
183        @Override
184        public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) {
185                validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource);
186
187                IResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
188                IResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource);
189
190                Optional<? extends IMdmLink> oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId);
191                if (oMdmLink.isEmpty()) {
192                        throw new InvalidRequestException(Msg.code(745) + "No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless());
193                }
194
195                IMdmLink mdmLink = oMdmLink.get();
196                if (!mdmLink.isPossibleDuplicate()) {
197                        throw new InvalidRequestException(Msg.code(746) + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE.");
198                }
199                mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH);
200                mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
201                myMdmLinkDaoSvc.save(mdmLink);
202        }
203
204        /**
205         * Ensure that the two resources are of the same type and both are managed by HAPI-MDM
206         */
207        private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) {
208                String goldenResourceType = myFhirContext.getResourceType(theGoldenResource);
209                String targetType = myFhirContext.getResourceType(theTarget);
210                if (!goldenResourceType.equalsIgnoreCase(targetType)) {
211                        throw new InvalidRequestException(Msg.code(747) + "First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be the same resource type as the second argument.  Was " + goldenResourceType + "/" + targetType);
212                }
213
214                if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) {
215                        throw new InvalidRequestException(Msg.code(748) + "Only MDM Managed Golden Resources may be updated via this operation.  The resource provided is not tagged as managed by HAPI-MDM");
216                }
217        }
218}