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