001/*-
002 * #%L
003 * HAPI FHIR - Master Data Management
004 * %%
005 * Copyright (C) 2014 - 2025 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.mdm.svc;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
025import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
026import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
027import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
028import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
029import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
030import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc;
031import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
032import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
033import ca.uhn.fhir.mdm.api.paging.MdmPageRequest;
034import ca.uhn.fhir.mdm.api.params.MdmQuerySearchParameters;
035import ca.uhn.fhir.mdm.model.MdmTransactionContext;
036import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
037import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
038import ca.uhn.fhir.rest.api.Constants;
039import ca.uhn.fhir.rest.api.server.RequestDetails;
040import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
041import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
042import ca.uhn.fhir.util.TerserUtil;
043import org.hl7.fhir.instance.model.api.IAnyResource;
044import org.hl7.fhir.instance.model.api.IBase;
045import org.hl7.fhir.instance.model.api.IBaseResource;
046import org.hl7.fhir.instance.model.api.IIdType;
047import org.springframework.data.domain.Page;
048
049import java.util.ArrayList;
050import java.util.HashMap;
051import java.util.List;
052import java.util.Map;
053import java.util.regex.Pattern;
054import java.util.stream.Stream;
055
056public class MdmSurvivorshipSvcImpl implements IMdmSurvivorshipService {
057        private static final Pattern IS_UUID =
058                        Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
059
060        protected final FhirContext myFhirContext;
061
062        private final GoldenResourceHelper myGoldenResourceHelper;
063
064        private final DaoRegistry myDaoRegistry;
065        private final IMdmLinkQuerySvc myMdmLinkQuerySvc;
066
067        private final IIdHelperService<?> myIIdHelperService;
068
069        private final HapiTransactionService myTransactionService;
070
071        public MdmSurvivorshipSvcImpl(
072                        FhirContext theFhirContext,
073                        GoldenResourceHelper theResourceHelper,
074                        DaoRegistry theDaoRegistry,
075                        IMdmLinkQuerySvc theLinkQuerySvc,
076                        IIdHelperService<?> theIIdHelperService,
077                        HapiTransactionService theHapiTransactionService) {
078                myFhirContext = theFhirContext;
079                myGoldenResourceHelper = theResourceHelper;
080                myDaoRegistry = theDaoRegistry;
081                myMdmLinkQuerySvc = theLinkQuerySvc;
082                myIIdHelperService = theIIdHelperService;
083                myTransactionService = theHapiTransactionService;
084        }
085
086        // this logic is custom in smile vs hapi
087        @Override
088        public <T extends IBase> void applySurvivorshipRulesToGoldenResource(
089                        T theTargetResource, T theGoldenResource, MdmTransactionContext theMdmTransactionContext) {
090                switch (theMdmTransactionContext.getRestOperation()) {
091                        case MERGE_GOLDEN_RESOURCES:
092                                TerserUtil.mergeFields(
093                                                myFhirContext,
094                                                (IBaseResource) theTargetResource,
095                                                (IBaseResource) theGoldenResource,
096                                                TerserUtil.EXCLUDE_IDS_AND_META);
097                                break;
098                        default:
099                                TerserUtil.replaceFields(
100                                                myFhirContext,
101                                                (IBaseResource) theTargetResource,
102                                                (IBaseResource) theGoldenResource,
103                                                TerserUtil.EXCLUDE_IDS_AND_META);
104                                break;
105                }
106        }
107
108        // This logic is the same for all implementations (including jpa or mongo)
109        @SuppressWarnings({"rawtypes", "unchecked"})
110        @Override
111        public <T extends IBase> T rebuildGoldenResourceWithSurvivorshipRules(
112                        RequestDetails theRequestDetails, T theGoldenResourceBase, MdmTransactionContext theMdmTransactionContext) {
113                IBaseResource goldenResource = (IBaseResource) theGoldenResourceBase;
114
115                // we want a list of source ids linked to this
116                // golden resource id; sorted and filtered for only MATCH results
117                Stream<IBaseResource> sourceResources =
118                                getMatchedSourceIdsByLinkUpdateDate(theRequestDetails, goldenResource, theMdmTransactionContext);
119
120                IBaseResource toSave = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(
121                                (IAnyResource) goldenResource,
122                                theMdmTransactionContext,
123                                null // we don't want to apply survivorship - just create a new GoldenResource
124                                );
125
126                toSave.setId(goldenResource.getIdElement().toUnqualifiedVersionless());
127
128                sourceResources.forEach(source -> {
129                        applySurvivorshipRulesToGoldenResource(source, toSave, theMdmTransactionContext);
130                });
131
132                // save it
133                IFhirResourceDao dao = myDaoRegistry.getResourceDao(goldenResource.fhirType());
134
135                SystemRequestDetails requestDetails = new SystemRequestDetails();
136                // if using partitions, we should save to the correct partition
137                Object resourcePartitionIdObj = toSave.getUserData(Constants.RESOURCE_PARTITION_ID);
138                if (resourcePartitionIdObj instanceof RequestPartitionId) {
139                        RequestPartitionId partitionId = (RequestPartitionId) resourcePartitionIdObj;
140                        requestDetails.setRequestPartitionId(partitionId);
141                }
142                dao.update(toSave, requestDetails);
143
144                return (T) toSave;
145        }
146
147        @SuppressWarnings("rawtypes")
148        private Stream<IBaseResource> getMatchedSourceIdsByLinkUpdateDate(
149                        RequestDetails theRequestDetails,
150                        IBaseResource theGoldenResource,
151                        MdmTransactionContext theMdmTransactionContext) {
152                String resourceType = theGoldenResource.fhirType();
153                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
154
155                MdmQuerySearchParameters searchParameters = new MdmQuerySearchParameters(new MdmPageRequest(0, 50, 50, 50));
156                searchParameters.setGoldenResourceId(theGoldenResource.getIdElement());
157                searchParameters.setSort("myUpdated");
158                searchParameters.setMatchResult(MdmMatchResultEnum.MATCH);
159                Page<MdmLinkJson> linksQuery = myMdmLinkQuerySvc.queryLinks(searchParameters, theMdmTransactionContext);
160
161                // we want it ordered
162                List<IIdType> sourceIds = new ArrayList<>();
163                linksQuery.forEach(link -> {
164                        String sourceId = link.getSourceId();
165                        if (!sourceId.contains("/")) {
166                                sourceId = resourceType + "/" + sourceId;
167                        }
168                        IIdType id = myFhirContext.getVersion().newIdType();
169                        id.setValue(sourceId);
170                        sourceIds.add(id);
171                });
172                Map<String, IResourcePersistentId> sourceIdToPid = new HashMap<>();
173                if (!sourceIds.isEmpty()) {
174                        // we cannot call resolveResourcePersistentIds if there are no ids to call it with
175                        myTransactionService
176                                        .withRequest(new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.allPartitions()))
177                                        .execute(() -> {
178                                                Map<IIdType, ? extends IResourceLookup<?>> ids = myIIdHelperService.resolveResourceIdentities(
179                                                                RequestPartitionId.allPartitions(),
180                                                                sourceIds,
181                                                                ResolveIdentityMode.includeDeleted().cacheOk());
182                                                for (Map.Entry<IIdType, ? extends IResourceLookup<?>> entry : ids.entrySet()) {
183                                                        sourceIdToPid.put(
184                                                                        entry.getKey().getIdPart(), entry.getValue().getPersistentId());
185                                                }
186                                        });
187                }
188
189                return sourceIds.stream().map(id -> {
190                        IResourcePersistentId<?> pid = sourceIdToPid.get(id.getIdPart());
191                        return dao.readByPid(pid);
192                });
193        }
194}