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.dao.index.IdHelperService;
027import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
028import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper;
029import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
030import ca.uhn.fhir.mdm.api.IMdmLink;
031import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
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.GoldenResourceHelper;
037import ca.uhn.fhir.mdm.util.MdmResourceUtil;
038import ca.uhn.fhir.rest.api.Constants;
039import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
040import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
041import org.hl7.fhir.instance.model.api.IAnyResource;
042import org.hl7.fhir.instance.model.api.IIdType;
043import org.slf4j.Logger;
044import org.springframework.beans.factory.annotation.Autowired;
045import org.springframework.stereotype.Service;
046import org.springframework.transaction.annotation.Transactional;
047
048import java.util.ArrayList;
049import java.util.List;
050import java.util.Optional;
051
052@Service
053public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc {
054
055        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
056
057        @Autowired
058        GoldenResourceHelper myGoldenResourceHelper;
059        @Autowired
060        MdmLinkDaoSvc myMdmLinkDaoSvc;
061        @Autowired
062        IMdmLinkSvc myMdmLinkSvc;
063        @Autowired
064        IIdHelperService myIdHelperService;
065        @Autowired
066        MdmResourceDaoSvc myMdmResourceDaoSvc;
067        @Autowired
068        MdmPartitionHelper myMdmPartitionHelper;
069
070        @Override
071        @Transactional
072        public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theMergedResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
073                String resourceType = theMdmTransactionContext.getResourceType();
074
075                if (theMergedResource != null) {
076                        if (myGoldenResourceHelper.hasIdentifier(theMergedResource)) {
077                                throw new IllegalArgumentException(Msg.code(751) + "Manually merged resource can not contain identifiers");
078                        }
079                        myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theMergedResource, theMdmTransactionContext);
080                        myGoldenResourceHelper.mergeIndentifierFields(theToGoldenResource, theMergedResource, theMdmTransactionContext);
081
082                        theMergedResource.setId(theToGoldenResource.getId());
083                        theToGoldenResource = (IAnyResource) myMdmResourceDaoSvc.upsertGoldenResource(theMergedResource, resourceType).getResource();
084                } else {
085                        myGoldenResourceHelper.mergeIndentifierFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
086                        myGoldenResourceHelper.mergeNonIdentiferFields(theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
087                        //Save changes to the golden resource
088                        myMdmResourceDaoSvc.upsertGoldenResource(theToGoldenResource, resourceType);
089                }
090
091                // check if the golden resource and the source resource are in the same partition, throw error if not
092                myMdmPartitionHelper.validateResourcesInSamePartition(theFromGoldenResource, theToGoldenResource);
093
094                //Merge the links from the FROM to the TO resource. Clean up dangling links.
095                mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, theFromGoldenResource.getIdElement(), theMdmTransactionContext);
096
097                //Create the new REDIRECT link
098                addMergeLink(theToGoldenResource, theFromGoldenResource, resourceType);
099
100                //Strip the golden resource tag from the now-deprecated resource.
101                myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType);
102
103                //Add the REDIRECT tag to that same deprecated resource.
104                MdmResourceUtil.setGoldenResourceRedirected(theFromGoldenResource);
105
106                //Save the deprecated resource.
107                myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType);
108
109                log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless()
110                        + " into " + theToGoldenResource.getIdElement().toVersionless());
111                return theToGoldenResource;
112        }
113
114        private void addMergeLink(IAnyResource theGoldenResource, IAnyResource theTargetResource, String theResourceType) {
115                IMdmLink mdmLink = myMdmLinkDaoSvc
116                        .getOrCreateMdmLinkByGoldenResourceAndSourceResource(theGoldenResource, theTargetResource);
117
118                mdmLink
119                        .setMdmSourceType(theResourceType)
120                        .setMatchResult(MdmMatchResultEnum.REDIRECT)
121                        .setLinkSource(MdmLinkSourceEnum.MANUAL);
122                myMdmLinkDaoSvc.save(mdmLink);
123        }
124
125
126        /**
127         * Helper method which performs merger of links between resources, and cleans up dangling links afterwards.
128         * <p>
129         * For each incomingLink, either ignore it, move it, or replace the original one
130         * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it.
131         * 2. If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource
132         * 3. If an incoming link is MANUAL, and theres a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one.
133         * 4. Manual link collisions cause invalid request exception.
134         *
135         * @param theFromResource
136         * @param theToResource
137         * @param theToResourcePid
138         * @param theMdmTransactionContext
139         */
140        private void mergeGoldenResourceLinks(IAnyResource theFromResource, IAnyResource theToResource, IIdType theToResourcePid, MdmTransactionContext theMdmTransactionContext) {
141                List<? extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource
142                List<? extends IMdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); // toLinks - links going to theToResource
143                List<IMdmLink> toDelete = new ArrayList<>();
144                ResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds((RequestPartitionId) theToResource.getUserData(Constants.RESOURCE_PARTITION_ID), theToResource.getIdElement().getResourceType(), theToResource.getIdElement().getIdPart());
145
146                for (IMdmLink fromLink : fromLinks) {
147                        Optional<? extends IMdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink);
148                        if (optionalToLink.isPresent()) {
149
150                                // The original links already contain this target, so move it over to the toResource
151                                IMdmLink toLink = optionalToLink.get();
152                                if (fromLink.isManual()) {
153                                        switch (toLink.getLinkSource()) {
154                                                case AUTO:
155                                                        //3
156                                                        log(theMdmTransactionContext, String.format("MANUAL overrides AUT0.  Deleting link %s", toLink.toString()));
157                                                        myMdmLinkDaoSvc.deleteLink(toLink);
158                                                        break;
159                                                case MANUAL:
160                                                        if (fromLink.getMatchResult() != toLink.getMatchResult()) {
161                                                                throw new InvalidRequestException(Msg.code(752) + "A MANUAL " + fromLink.getMatchResult() + " link may not be merged into a MANUAL " + toLink.getMatchResult() + " link for the same target");
162                                                        }
163                                        }
164                                } else {
165                                        //1
166                                        toDelete.add(fromLink);
167                                        continue;
168                                }
169                        }
170                        //2 The original TO links didn't contain this target, so move it over to the toGoldenResource
171                        fromLink.setGoldenResourcePersistenceId(goldenResourcePid);
172                        ourLog.trace("Saving link {}", fromLink);
173                        myMdmLinkDaoSvc.save(fromLink);
174                }
175                //1 Delete dangling links
176                toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link));
177        }
178
179        private Optional<? extends IMdmLink> findFirstLinkWithMatchingSource(List<? extends IMdmLink> theMdmLinks, IMdmLink theLinkWithSourceToMatch) {
180                return theMdmLinks.stream()
181                        .filter(mdmLink -> mdmLink.getSourcePersistenceId().equals(theLinkWithSourceToMatch.getSourcePersistenceId()))
182                        .findFirst();
183        }
184
185        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
186                theMdmTransactionContext.addTransactionLogMessage(theMessage);
187                ourLog.debug(theMessage);
188        }
189}