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.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.mdm.util.MdmPartitionHelper;
028import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
029import ca.uhn.fhir.mdm.api.IMdmLink;
030import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
031import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
032import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
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.IResourcePersistentId;
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, theMdmTransactionContext);
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        /**
115         * This connects 2 golden resources (GR and TR here)
116         *
117         * 1 Deletes any current links: TR, ?, ?, GR
118         * 2 Creates a new link: GR, MANUAL, REDIRECT, TR
119         *
120         * Before:
121         * TR -> GR
122         *
123         * After:
124         * GR -> TR
125         */
126        private void addMergeLink(
127                IAnyResource theGoldenResource,
128                IAnyResource theTargetResource,
129                String theResourceType,
130                MdmTransactionContext theMdmTransactionContext
131        ) {
132                myMdmLinkSvc.deleteLink(theGoldenResource, theTargetResource,
133                        theMdmTransactionContext);
134
135                myMdmLinkDaoSvc.createOrUpdateLinkEntity(
136                        theTargetResource, // golden / LHS
137                        theGoldenResource, // source / RHS
138                        new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.REDIRECT),
139                        MdmLinkSourceEnum.MANUAL,
140                        theMdmTransactionContext // mdm transaction context
141                );
142        }
143
144        private RequestPartitionId getPartitionIdForResource(IAnyResource theResource) {
145                RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID);
146                // TODO - this seems to be null on the put with
147                // client id (forced id). Is this a bug?
148                if (partitionId == null) {
149                        partitionId = RequestPartitionId.allPartitions();
150                }
151                return partitionId;
152        }
153
154        /**
155         * Helper method which performs merger of links between resources, and cleans up dangling links afterwards.
156         * <p>
157         * For each incomingLink, either ignore it, move it, or replace the original one
158         * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it.
159         * 2.a If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource
160         * 2.b If the link does not exist on the TO resource, but is actually self-referential, it will just be removed
161         * 3. If an incoming link is MANUAL, and there's a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one.
162         * 4. Manual link collisions cause invalid request exception.
163         *
164         * @param theFromResource
165         * @param theToResource
166         * @param theToResourcePid
167         * @param theMdmTransactionContext
168         */
169        private void mergeGoldenResourceLinks(
170                IAnyResource theFromResource,
171                IAnyResource theToResource,
172                IIdType theToResourcePid,
173                MdmTransactionContext theMdmTransactionContext
174        ) {
175                // fromLinks - links from theFromResource to any resource
176                List<? extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource);
177                // toLinks - links from theToResource to any resource
178                List<? extends IMdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource);
179                List<IMdmLink> toDelete = new ArrayList<>();
180
181                IResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds(
182                        getPartitionIdForResource(theToResource),
183                        theToResource.getIdElement().getResourceType(),
184                        theToResource.getIdElement().getIdPart()
185                );
186
187                // reassign links:
188                // to <- from
189                for (IMdmLink fromLink : fromLinks) {
190                        Optional<? extends IMdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink);
191                        if (optionalToLink.isPresent()) {
192
193                                // The original links already contain this target, so move it over to the toResource
194                                IMdmLink toLink = optionalToLink.get();
195                                if (fromLink.isManual()) {
196                                        switch (toLink.getLinkSource()) {
197                                                case AUTO:
198                                                        //3
199                                                        log(theMdmTransactionContext, String.format("MANUAL overrides AUT0.  Deleting link %s", toLink.toString()));
200                                                        myMdmLinkDaoSvc.deleteLink(toLink);
201                                                        break;
202                                                case MANUAL:
203                                                        if (fromLink.getMatchResult() != toLink.getMatchResult()) {
204                                                                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");
205                                                        }
206                                        }
207                                } else {
208                                        // 1
209                                        toDelete.add(fromLink);
210                                        continue;
211                                }
212                        }
213
214                        if (fromLink.getSourcePersistenceId().equals(goldenResourcePid)) {
215                                // 2.b if the link is going to be self-referential we'll just delete it
216                                // (ie, do not link back to itself)
217                                myMdmLinkDaoSvc.deleteLink(fromLink);
218                        } else {
219                                // 2.a The original TO links didn't contain this target, so move it over to the toGoldenResource.
220                                fromLink.setGoldenResourcePersistenceId(goldenResourcePid);
221                                ourLog.trace("Saving link {}", fromLink);
222                                myMdmLinkDaoSvc.save(fromLink);
223                        }
224                }
225
226                // 1 Delete dangling links
227                toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link));
228        }
229
230        private Optional<? extends IMdmLink> findFirstLinkWithMatchingSource(List<? extends IMdmLink> theMdmLinks, IMdmLink theLinkWithSourceToMatch) {
231                return theMdmLinks.stream()
232                        .filter(mdmLink -> mdmLink.getSourcePersistenceId().equals(theLinkWithSourceToMatch.getSourcePersistenceId()))
233                        .findFirst();
234        }
235
236        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
237                theMdmTransactionContext.addTransactionLogMessage(theMessage);
238                ourLog.debug(theMessage);
239        }
240}