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.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
025import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
026import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
027import ca.uhn.fhir.mdm.api.IMdmLink;
028import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
029import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
030import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
031import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
032import ca.uhn.fhir.mdm.log.Logs;
033import ca.uhn.fhir.mdm.model.MdmTransactionContext;
034import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
035import ca.uhn.fhir.mdm.util.MdmPartitionHelper;
036import ca.uhn.fhir.mdm.util.MdmResourceUtil;
037import ca.uhn.fhir.rest.api.Constants;
038import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
039import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
040import org.hl7.fhir.instance.model.api.IAnyResource;
041import org.hl7.fhir.instance.model.api.IIdType;
042import org.slf4j.Logger;
043import org.springframework.beans.factory.annotation.Autowired;
044import org.springframework.stereotype.Service;
045import org.springframework.transaction.annotation.Transactional;
046
047import java.util.ArrayList;
048import java.util.List;
049import java.util.Optional;
050
051@Service
052public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc {
053
054        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
055
056        @Autowired
057        GoldenResourceHelper myGoldenResourceHelper;
058
059        @Autowired
060        MdmLinkDaoSvc myMdmLinkDaoSvc;
061
062        @Autowired
063        IMdmLinkSvc myMdmLinkSvc;
064
065        @Autowired
066        IIdHelperService myIdHelperService;
067
068        @Autowired
069        MdmResourceDaoSvc myMdmResourceDaoSvc;
070
071        @Autowired
072        MdmPartitionHelper myMdmPartitionHelper;
073
074        @Override
075        @Transactional
076        public IAnyResource mergeGoldenResources(
077                        IAnyResource theFromGoldenResource,
078                        IAnyResource theMergedResource,
079                        IAnyResource theToGoldenResource,
080                        MdmTransactionContext theMdmTransactionContext) {
081                String resourceType = theMdmTransactionContext.getResourceType();
082
083                if (theMergedResource != null) {
084                        if (myGoldenResourceHelper.hasIdentifier(theMergedResource)) {
085                                throw new IllegalArgumentException(
086                                                Msg.code(751) + "Manually merged resource can not contain identifiers");
087                        }
088                        myGoldenResourceHelper.mergeIndentifierFields(
089                                        theFromGoldenResource, theMergedResource, theMdmTransactionContext);
090                        myGoldenResourceHelper.mergeIndentifierFields(
091                                        theToGoldenResource, theMergedResource, theMdmTransactionContext);
092
093                        theMergedResource.setId(theToGoldenResource.getId());
094                        theToGoldenResource = (IAnyResource) myMdmResourceDaoSvc
095                                        .upsertGoldenResource(theMergedResource, resourceType)
096                                        .getResource();
097                } else {
098                        myGoldenResourceHelper.mergeIndentifierFields(
099                                        theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
100                        myGoldenResourceHelper.mergeNonIdentiferFields(
101                                        theFromGoldenResource, theToGoldenResource, theMdmTransactionContext);
102                        // Save changes to the golden resource
103                        myMdmResourceDaoSvc.upsertGoldenResource(theToGoldenResource, resourceType);
104                }
105
106                myMdmPartitionHelper.validateMdmResourcesPartitionMatches(theFromGoldenResource, theToGoldenResource);
107
108                // Merge the links from the FROM to the TO resource. Clean up dangling links.
109                mergeGoldenResourceLinks(
110                                theFromGoldenResource,
111                                theToGoldenResource,
112                                theFromGoldenResource.getIdElement(),
113                                theMdmTransactionContext);
114
115                // Create the new REDIRECT link
116                addMergeLink(theToGoldenResource, theFromGoldenResource, resourceType, theMdmTransactionContext);
117
118                // Strip the golden resource tag from the now-deprecated resource.
119                myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType);
120
121                // Add the REDIRECT tag to that same deprecated resource.
122                MdmResourceUtil.setGoldenResourceRedirected(theFromGoldenResource);
123
124                // Save the deprecated resource.
125                myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType);
126
127                log(
128                                theMdmTransactionContext,
129                                "Merged " + theFromGoldenResource.getIdElement().toVersionless() + " into "
130                                                + theToGoldenResource.getIdElement().toVersionless());
131                return theToGoldenResource;
132        }
133
134        /**
135         * This connects 2 golden resources (GR and TR here)
136         *
137         * 1 Deletes any current links: TR, ?, ?, GR
138         * 2 Creates a new link: GR, MANUAL, REDIRECT, TR
139         *
140         * Before:
141         * TR -> GR
142         *
143         * After:
144         * GR -> TR
145         */
146        private void addMergeLink(
147                        IAnyResource theGoldenResource,
148                        IAnyResource theTargetResource,
149                        String theResourceType,
150                        MdmTransactionContext theMdmTransactionContext) {
151                myMdmLinkSvc.deleteLink(theGoldenResource, theTargetResource, theMdmTransactionContext);
152
153                myMdmLinkDaoSvc.createOrUpdateLinkEntity(
154                                theTargetResource, // golden / LHS
155                                theGoldenResource, // source / RHS
156                                new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.REDIRECT),
157                                MdmLinkSourceEnum.MANUAL,
158                                theMdmTransactionContext // mdm transaction context
159                                );
160        }
161
162        private RequestPartitionId getPartitionIdForResource(IAnyResource theResource) {
163                RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID);
164                // TODO - this seems to be null on the put with
165                // client id (forced id). Is this a bug?
166                if (partitionId == null) {
167                        partitionId = RequestPartitionId.allPartitions();
168                }
169                return partitionId;
170        }
171
172        /**
173         * Helper method which performs merger of links between resources, and cleans up dangling links afterwards.
174         * <p>
175         * For each incomingLink, either ignore it, move it, or replace the original one
176         * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it.
177         * 2.a If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource
178         * 2.b If the link does not exist on the TO resource, but is actually self-referential, it will just be removed
179         * 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.
180         * 4. Manual link collisions cause invalid request exception.
181         *
182         * @param theFromResource
183         * @param theToResource
184         * @param theToResourcePid
185         * @param theMdmTransactionContext
186         */
187        private void mergeGoldenResourceLinks(
188                        IAnyResource theFromResource,
189                        IAnyResource theToResource,
190                        IIdType theToResourcePid,
191                        MdmTransactionContext theMdmTransactionContext) {
192                // fromLinks - links from theFromResource to any resource
193                List<? extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource);
194                // toLinks - links from theToResource to any resource
195                List<? extends IMdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource);
196                List<IMdmLink> toDelete = new ArrayList<>();
197
198                IResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds(
199                                getPartitionIdForResource(theToResource),
200                                theToResource.getIdElement().getResourceType(),
201                                theToResource.getIdElement().getIdPart());
202
203                // reassign links:
204                // to <- from
205                for (IMdmLink fromLink : fromLinks) {
206                        Optional<? extends IMdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink);
207                        if (optionalToLink.isPresent()) {
208
209                                // The original links already contain this target, so move it over to the toResource
210                                IMdmLink toLink = optionalToLink.get();
211                                if (fromLink.isManual()) {
212                                        switch (toLink.getLinkSource()) {
213                                                case AUTO:
214                                                        // 3
215                                                        log(
216                                                                        theMdmTransactionContext,
217                                                                        String.format("MANUAL overrides AUT0.  Deleting link %s", toLink.toString()));
218                                                        myMdmLinkDaoSvc.deleteLink(toLink);
219                                                        break;
220                                                case MANUAL:
221                                                        if (fromLink.getMatchResult() != toLink.getMatchResult()) {
222                                                                throw new InvalidRequestException(Msg.code(752) + "A MANUAL "
223                                                                                + fromLink.getMatchResult() + " link may not be merged into a MANUAL "
224                                                                                + toLink.getMatchResult() + " link for the same target");
225                                                        }
226                                        }
227                                } else {
228                                        // 1
229                                        toDelete.add(fromLink);
230                                        continue;
231                                }
232                        }
233
234                        if (fromLink.getSourcePersistenceId().equals(goldenResourcePid)) {
235                                // 2.b if the link is going to be self-referential we'll just delete it
236                                // (ie, do not link back to itself)
237                                myMdmLinkDaoSvc.deleteLink(fromLink);
238                        } else {
239                                // 2.a The original TO links didn't contain this target, so move it over to the toGoldenResource.
240                                fromLink.setGoldenResourcePersistenceId(goldenResourcePid);
241                                ourLog.trace("Saving link {}", fromLink);
242                                myMdmLinkDaoSvc.save(fromLink);
243                        }
244                }
245
246                // 1 Delete dangling links
247                toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link));
248        }
249
250        private Optional<? extends IMdmLink> findFirstLinkWithMatchingSource(
251                        List<? extends IMdmLink> theMdmLinks, IMdmLink theLinkWithSourceToMatch) {
252                return theMdmLinks.stream()
253                                .filter(mdmLink ->
254                                                mdmLink.getSourcePersistenceId().equals(theLinkWithSourceToMatch.getSourcePersistenceId()))
255                                .findFirst();
256        }
257
258        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
259                theMdmTransactionContext.addTransactionLogMessage(theMessage);
260                ourLog.debug(theMessage);
261        }
262}