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