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.context.FhirContext;
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.jpa.mdm.util.MdmPartitionHelper;
028import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.mdm.api.IMdmLink;
031import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
032import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
033import ca.uhn.fhir.mdm.api.IMdmSettings;
034import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
035import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
036import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
037import ca.uhn.fhir.mdm.log.Logs;
038import ca.uhn.fhir.mdm.model.MdmTransactionContext;
039import ca.uhn.fhir.mdm.util.MdmResourceUtil;
040import ca.uhn.fhir.mdm.util.MessageHelper;
041import ca.uhn.fhir.rest.api.Constants;
042import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
043import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
044import ca.uhn.fhir.rest.server.provider.ProviderConstants;
045import org.hl7.fhir.instance.model.api.IAnyResource;
046import org.slf4j.Logger;
047import org.springframework.beans.factory.annotation.Autowired;
048import org.springframework.transaction.annotation.Transactional;
049
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        IMdmLinkSvc myMdmLinkSvc;
065        @Autowired
066        MdmResourceDaoSvc myMdmResourceDaoSvc;
067        @Autowired
068        MdmMatchLinkSvc myMdmMatchLinkSvc;
069        @Autowired
070        IMdmSettings myMdmSettings;
071        @Autowired
072        MessageHelper myMessageHelper;
073        @Autowired
074        IMdmSurvivorshipService myMdmSurvivorshipService;
075        @Autowired
076        MdmPartitionHelper myMdmPartitionHelper;
077
078        @Transactional
079        @Override
080        public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) {
081                String sourceType = myFhirContext.getResourceType(theSourceResource);
082
083                validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType);
084
085                ResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
086                ResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theSourceResource);
087
088                // check if the golden resource and the source resource are in the same partition, throw error if not
089                myMdmPartitionHelper.validateResourcesInSamePartition(theGoldenResource, theSourceResource);
090
091                Optional<? extends IMdmLink> optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId);
092                if (!optionalMdmLink.isPresent()) {
093                        throw new InvalidRequestException(Msg.code(738) + myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource));
094                }
095
096                IMdmLink mdmLink = optionalMdmLink.get();
097                if (mdmLink.getMatchResult() == theMatchResult) {
098                        ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ".  Nothing to do.");
099                        return theGoldenResource;
100                }
101
102                ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + ".");
103                mdmLink.setMatchResult(theMatchResult);
104                mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
105
106                // Add partition for the mdm link if it doesn't exist
107                RequestPartitionId goldenResourcePartitionId = (RequestPartitionId) theGoldenResource.getUserData(Constants.RESOURCE_PARTITION_ID);
108                if (goldenResourcePartitionId != null && goldenResourcePartitionId.hasPartitionIds() && goldenResourcePartitionId.getFirstPartitionIdOrNull() != null &&
109                        (mdmLink.getPartitionId() == null || mdmLink.getPartitionId().getPartitionId() == null)) {
110                        mdmLink.setPartitionId(new PartitionablePartitionId(goldenResourcePartitionId.getFirstPartitionIdOrNull(), goldenResourcePartitionId.getPartitionDate()));
111                }
112                myMdmLinkDaoSvc.save(mdmLink);
113
114                if (theMatchResult == MdmMatchResultEnum.MATCH) {
115                        // only apply survivorship rules in case of a match
116                        myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theSourceResource, theGoldenResource, theMdmContext);
117                }
118
119                myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType());
120                if (theMatchResult == MdmMatchResultEnum.NO_MATCH) {
121                        // Need to find a new Golden Resource to link this target to
122                        myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext);
123                }
124                return theGoldenResource;
125        }
126
127        private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) {
128                String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord);
129
130                if (theMatchResult != MdmMatchResultEnum.NO_MATCH &&
131                        theMatchResult != MdmMatchResultEnum.MATCH) {
132                        throw new InvalidRequestException(Msg.code(739) + myMessageHelper.getMessageForUnsupportedMatchResult());
133                }
134
135                if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) {
136                        throw new InvalidRequestException(Msg.code(740) + myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType));
137                }
138
139                if (!myMdmSettings.isSupportedMdmType(theSourceType)) {
140                        throw new InvalidRequestException(Msg.code(741) + myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType));
141                }
142
143                if (!Objects.equals(goldenRecordType, theSourceType)) {
144                        throw new InvalidRequestException(Msg.code(742) + myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType));
145                }
146
147                if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) {
148                        throw new InvalidRequestException(Msg.code(743) + myMessageHelper.getMessageForUnmanagedResource());
149                }
150
151                if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) {
152                        throw new InvalidRequestException(Msg.code(744) + myMessageHelper.getMessageForUnsupportedSourceResource());
153                }
154        }
155
156        @Transactional
157        @Override
158        public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) {
159                validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource);
160
161                ResourcePersistentId goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
162                ResourcePersistentId targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource);
163
164                Optional<? extends IMdmLink> oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId);
165                if (!oMdmLink.isPresent()) {
166                        throw new InvalidRequestException(Msg.code(745) + "No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless());
167                }
168
169                IMdmLink mdmLink = oMdmLink.get();
170                if (!mdmLink.isPossibleDuplicate()) {
171                        throw new InvalidRequestException(Msg.code(746) + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE.");
172                }
173                mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH);
174                mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
175                myMdmLinkDaoSvc.save(mdmLink);
176        }
177
178        /**
179         * Ensure that the two resources are of the same type and both are managed by HAPI-MDM
180         */
181        private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) {
182                String goldenResourceType = myFhirContext.getResourceType(theGoldenResource);
183                String targetType = myFhirContext.getResourceType(theTarget);
184                if (!goldenResourceType.equalsIgnoreCase(targetType)) {
185                        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);
186                }
187
188                if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) {
189                        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");
190                }
191        }
192}