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