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.util;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.context.FhirVersionEnum;
026import ca.uhn.fhir.context.RuntimeResourceDefinition;
027import ca.uhn.fhir.fhirpath.IFhirPath;
028import ca.uhn.fhir.i18n.Msg;
029import ca.uhn.fhir.mdm.api.IMdmSettings;
030import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService;
031import ca.uhn.fhir.mdm.log.Logs;
032import ca.uhn.fhir.mdm.model.CanonicalEID;
033import ca.uhn.fhir.mdm.model.MdmTransactionContext;
034import ca.uhn.fhir.rest.api.Constants;
035import ca.uhn.fhir.util.FhirTerser;
036import jakarta.annotation.Nonnull;
037import org.hl7.fhir.instance.model.api.IAnyResource;
038import org.hl7.fhir.instance.model.api.IBase;
039import org.hl7.fhir.instance.model.api.IBaseResource;
040import org.hl7.fhir.instance.model.api.IPrimitiveType;
041import org.slf4j.Logger;
042import org.springframework.beans.factory.annotation.Autowired;
043import org.springframework.stereotype.Service;
044
045import java.util.ArrayList;
046import java.util.List;
047import java.util.Objects;
048import java.util.Optional;
049import java.util.stream.Collectors;
050
051import static ca.uhn.fhir.context.FhirVersionEnum.DSTU3;
052import static ca.uhn.fhir.context.FhirVersionEnum.R4;
053import static ca.uhn.fhir.context.FhirVersionEnum.R5;
054
055@Service
056public class GoldenResourceHelper {
057
058        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
059
060        static final String FIELD_NAME_IDENTIFIER = "identifier";
061
062        private final IMdmSettings myMdmSettings;
063
064        private final EIDHelper myEIDHelper;
065
066        private final MdmPartitionHelper myMdmPartitionHelper;
067
068        private final FhirContext myFhirContext;
069
070        @Autowired
071        public GoldenResourceHelper(
072                        FhirContext theFhirContext,
073                        IMdmSettings theMdmSettings,
074                        EIDHelper theEIDHelper,
075                        MdmPartitionHelper theMdmPartitionHelper) {
076                myFhirContext = theFhirContext;
077                myMdmSettings = theMdmSettings;
078                myEIDHelper = theEIDHelper;
079                myMdmPartitionHelper = theMdmPartitionHelper;
080        }
081
082        /**
083         * Creates a copy of the specified resource. This method will carry over resource EID if it exists. If it does not exist,
084         * a randomly generated UUID EID will be created.
085         *
086         * @param <T>                      Supported MDM resource type (e.g. Patient, Practitioner)
087         * @param theIncomingResource     The resource to build the golden resource off of.
088         *                                 Could be the source resource or another golden resource.
089         *                                 If a golden resource, do not provide an IMdmSurvivorshipService
090         * @param theMdmTransactionContext The mdm transaction context
091         * @param theMdmSurvivorshipService IMdmSurvivorshipSvc. Provide only if survivorshipskills are desired
092         *                                  to be applied. Provide null otherwise.
093         */
094        @Nonnull
095        public <T extends IAnyResource> T createGoldenResourceFromMdmSourceResource(
096                        T theIncomingResource,
097                        MdmTransactionContext theMdmTransactionContext,
098                        IMdmSurvivorshipService theMdmSurvivorshipService) {
099                validateContextSupported();
100
101                // get a ref to the actual ID Field
102                RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theIncomingResource);
103                IBaseResource newGoldenResource = resourceDefinition.newInstance();
104
105                if (theMdmSurvivorshipService != null) {
106                        theMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(
107                                        theIncomingResource, newGoldenResource, theMdmTransactionContext);
108                }
109
110                // hapi has 2 metamodels: for children and types
111                BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
112
113                cloneMDMEidsIntoNewGoldenResource(goldenResourceIdentifier, theIncomingResource, newGoldenResource);
114
115                addHapiEidIfNoExternalEidIsPresent(newGoldenResource, goldenResourceIdentifier, theIncomingResource);
116
117                MdmResourceUtil.setMdmManaged(newGoldenResource);
118                MdmResourceUtil.setGoldenResource(newGoldenResource);
119
120                // TODO - on updating links, if resolving a link, this should go away?
121                // blocked resource's golden resource will be marked special
122                // they are not part of MDM matching algorithm (will not link to other resources)
123                // but other resources can link to them
124                if (theMdmTransactionContext.getIsBlocked()) {
125                        MdmResourceUtil.setGoldenResourceAsBlockedResourceGoldenResource(newGoldenResource);
126                }
127
128                // add the partition id to the new resource
129                newGoldenResource.setUserData(
130                                Constants.RESOURCE_PARTITION_ID,
131                                myMdmPartitionHelper.getRequestPartitionIdForNewGoldenResources(theIncomingResource));
132
133                return (T) newGoldenResource;
134        }
135
136        /**
137         * If there are no external EIDs on the incoming resource, create a new HAPI EID on the new Golden Resource.
138         */
139        // TODO GGG ask james if there is any way we can convert this canonical EID into a generic STU-agnostic IBase.
140        private <T extends IAnyResource> void addHapiEidIfNoExternalEidIsPresent(
141                        IBaseResource theNewGoldenResource,
142                        BaseRuntimeChildDefinition theGoldenResourceIdentifier,
143                        IAnyResource theSourceResource) {
144
145                List<CanonicalEID> eidsToApply = myEIDHelper.getExternalEid(theNewGoldenResource);
146                if (!eidsToApply.isEmpty()) {
147                        return;
148                }
149
150                CanonicalEID hapiEid = myEIDHelper.createHapiEid();
151                theGoldenResourceIdentifier
152                                .getMutator()
153                                .addValue(theNewGoldenResource, IdentifierUtil.toId(myFhirContext, hapiEid));
154
155                // set identifier on the source resource
156                cloneEidIntoResource(myFhirContext, theSourceResource, hapiEid);
157        }
158
159        private void cloneMDMEidsIntoNewGoldenResource(
160                        BaseRuntimeChildDefinition theGoldenResourceIdentifier,
161                        IAnyResource theIncomingResource,
162                        IBaseResource theNewGoldenResource) {
163                String incomingResourceType = myFhirContext.getResourceType(theIncomingResource);
164                String mdmEIDSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(incomingResourceType);
165
166                if (mdmEIDSystem == null) {
167                        return;
168                }
169
170                // FHIR choice types - fields within fhir where we have a choice of ids
171                IFhirPath fhirPath = myFhirContext.newFhirPath();
172                List<IBase> incomingResourceIdentifiers =
173                                theGoldenResourceIdentifier.getAccessor().getValues(theIncomingResource);
174
175                for (IBase incomingResourceIdentifier : incomingResourceIdentifiers) {
176                        Optional<IPrimitiveType> incomingIdentifierSystem =
177                                        fhirPath.evaluateFirst(incomingResourceIdentifier, "system", IPrimitiveType.class);
178                        if (incomingIdentifierSystem.isPresent()) {
179                                String incomingIdentifierSystemString =
180                                                incomingIdentifierSystem.get().getValueAsString();
181                                if (Objects.equals(incomingIdentifierSystemString, mdmEIDSystem)) {
182                                        ourLog.debug(
183                                                        "Incoming resource EID System {} matches EID system in the MDM rules.  Copying to Golden Resource.",
184                                                        incomingIdentifierSystemString);
185                                        ca.uhn.fhir.util.TerserUtil.cloneIdentifierIntoResource(
186                                                        myFhirContext,
187                                                        theGoldenResourceIdentifier,
188                                                        incomingResourceIdentifier,
189                                                        theNewGoldenResource);
190                                } else {
191                                        ourLog.debug(
192                                                        "Incoming resource EID System {} differs from EID system in the MDM rules {}.  Not copying to Golden Resource.",
193                                                        incomingIdentifierSystemString,
194                                                        mdmEIDSystem);
195                                }
196                        } else {
197                                ourLog.debug("No EID System in incoming resource.");
198                        }
199                }
200        }
201
202        private void validateContextSupported() {
203                FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion();
204                if (fhirVersion == R4 || fhirVersion == DSTU3 || fhirVersion == R5) {
205                        return;
206                }
207                throw new UnsupportedOperationException(Msg.code(1489) + "Version not supported: "
208                                + myFhirContext.getVersion().getVersion());
209        }
210
211        /**
212         * Updates EID on Golden Resource, based on the incoming source resource. If the incoming resource has an external EID, it is applied
213         * to the Golden Resource, unless that golden resource already has an external EID which does not match, in which case throw {@link IllegalArgumentException}
214         * <p>
215         * If running in multiple EID mode, then incoming EIDs are simply added to the Golden Resource without checking for matches.
216         *
217         * @param theGoldenResource The golden resource to update the external EID on.
218         * @param theSourceResource The source we will retrieve the external EID from.
219         * @return the modified {@link IBaseResource} representing the Golden Resource.
220         */
221        public IAnyResource updateGoldenResourceExternalEidFromSourceResource(
222                        IAnyResource theGoldenResource,
223                        IAnyResource theSourceResource,
224                        MdmTransactionContext theMdmTransactionContext) {
225                // This handles overwriting an automatically assigned EID if a patient that links is coming in with an official
226                // EID.
227                List<CanonicalEID> incomingSourceEid = myEIDHelper.getExternalEid(theSourceResource);
228                List<CanonicalEID> goldenResourceOfficialEid = myEIDHelper.getExternalEid(theGoldenResource);
229
230                if (incomingSourceEid.isEmpty()) {
231                        return theGoldenResource;
232                }
233
234                if (goldenResourceOfficialEid.isEmpty() || !myMdmSettings.isPreventMultipleEids()) {
235                        if (addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, incomingSourceEid)) {
236                                log(
237                                                theMdmTransactionContext,
238                                                "Incoming resource:" + theSourceResource.getIdElement().toUnqualifiedVersionless()
239                                                                + " + with EID "
240                                                                + incomingSourceEid.stream()
241                                                                                .map(CanonicalEID::toString)
242                                                                                .collect(Collectors.joining(","))
243                                                                + " is applying this EID to its related Golden Resource, as this Golden Resource does not yet have an external EID");
244                        }
245                } else if (!goldenResourceOfficialEid.isEmpty()
246                                && myEIDHelper.eidMatchExists(goldenResourceOfficialEid, incomingSourceEid)) {
247                        log(
248                                        theMdmTransactionContext,
249                                        "Incoming resource:" + theSourceResource.getIdElement().toVersionless() + " with EIDs "
250                                                        + incomingSourceEid.stream()
251                                                                        .map(CanonicalEID::toString)
252                                                                        .collect(Collectors.joining(","))
253                                                        + " does not need to overwrite the EID in the Golden Resource, as this EID is already present in the Golden Resource");
254                } else {
255                        throw new IllegalArgumentException(Msg.code(1490)
256                                        + String.format(
257                                                        "Incoming resource EID %s would create a duplicate Golden Resource, as Golden Resource EID %s already exists!",
258                                                        incomingSourceEid.toString(), goldenResourceOfficialEid.toString()));
259                }
260                return theGoldenResource;
261        }
262
263        public IBaseResource overwriteExternalEids(IBaseResource theGoldenResource, List<CanonicalEID> theNewEid) {
264                clearExternalEids(theGoldenResource);
265                addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, theNewEid);
266                return theGoldenResource;
267        }
268
269        private void clearExternalEidsFromTheGoldenResource(
270                        BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBaseResource theGoldenResource) {
271                IFhirPath fhirPath = myFhirContext.newFhirPath();
272                List<IBase> goldenResourceIdentifiers =
273                                theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource);
274                List<IBase> clonedIdentifiers = new ArrayList<>();
275                FhirTerser terser = myFhirContext.newTerser();
276
277                for (IBase base : goldenResourceIdentifiers) {
278                        Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class);
279                        if (system.isPresent()) {
280                                String resourceType = myFhirContext.getResourceType(theGoldenResource);
281                                String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType);
282                                String baseSystem = system.get().getValueAsString();
283                                if (Objects.equals(baseSystem, mdmSystem)) {
284                                        ourLog.debug(
285                                                        "Found EID confirming to MDM rules {}. It does not need to be copied, skipping",
286                                                        baseSystem);
287                                        continue;
288                                }
289                        }
290
291                        BaseRuntimeElementCompositeDefinition<?> childIdentifier = (BaseRuntimeElementCompositeDefinition<?>)
292                                        theGoldenResourceIdentifier.getChildByName(FIELD_NAME_IDENTIFIER);
293                        IBase goldenResourceNewIdentifier = childIdentifier.newInstance();
294                        terser.cloneInto(base, goldenResourceNewIdentifier, true);
295
296                        clonedIdentifiers.add(goldenResourceNewIdentifier);
297                }
298
299                goldenResourceIdentifiers.clear();
300                goldenResourceIdentifiers.addAll(clonedIdentifiers);
301        }
302
303        private void clearExternalEids(IBaseResource theGoldenResource) {
304                // validate the system - if it's set to EID system - then clear it - type and STU version
305                validateContextSupported();
306
307                // get a ref to the actual ID Field
308                RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theGoldenResource);
309                BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
310                clearExternalEidsFromTheGoldenResource(goldenResourceIdentifier, theGoldenResource);
311        }
312
313        /**
314         * Given a list of incoming External EIDs, and a Golden Resource, apply all the EIDs to this resource, which did not already exist on it.
315         * @return true if an EID was added
316         */
317        private boolean addCanonicalEidsToGoldenResourceIfAbsent(
318                        IBaseResource theGoldenResource, List<CanonicalEID> theIncomingSourceExternalEids) {
319                List<CanonicalEID> goldenResourceExternalEids = myEIDHelper.getExternalEid(theGoldenResource);
320                boolean addedEid = false;
321                for (CanonicalEID incomingExternalEid : theIncomingSourceExternalEids) {
322                        if (goldenResourceExternalEids.contains(incomingExternalEid)) {
323                                continue;
324                        }
325                        cloneEidIntoResource(myFhirContext, theGoldenResource, incomingExternalEid);
326                        addedEid = true;
327                }
328                return addedEid;
329        }
330
331        public boolean hasIdentifier(IBaseResource theResource) {
332                return ca.uhn.fhir.util.TerserUtil.hasValues(myFhirContext, theResource, FIELD_NAME_IDENTIFIER);
333        }
334
335        public void mergeIndentifierFields(
336                        IBaseResource theFromGoldenResource,
337                        IBaseResource theToGoldenResource,
338                        MdmTransactionContext theMdmTransactionContext) {
339                ca.uhn.fhir.util.TerserUtil.cloneCompositeField(
340                                myFhirContext, theFromGoldenResource, theToGoldenResource, FIELD_NAME_IDENTIFIER);
341        }
342
343        /**
344         * An incoming resource is a potential duplicate if it matches a source that has a golden resource with an official
345         * EID, but the incoming resource also has an EID that does not match.
346         */
347        public boolean isPotentialDuplicate(
348                        IAnyResource theExistingGoldenResource, IAnyResource theComparingGoldenResource) {
349                List<CanonicalEID> externalEidsGoldenResource = myEIDHelper.getExternalEid(theExistingGoldenResource);
350                List<CanonicalEID> externalEidsResource = myEIDHelper.getExternalEid(theComparingGoldenResource);
351                return !externalEidsGoldenResource.isEmpty()
352                                && !externalEidsResource.isEmpty()
353                                && !myEIDHelper.eidMatchExists(externalEidsResource, externalEidsGoldenResource);
354        }
355
356        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
357                theMdmTransactionContext.addTransactionLogMessage(theMessage);
358                ourLog.debug(theMessage);
359        }
360
361        public void handleExternalEidAddition(
362                        IAnyResource theGoldenResource,
363                        IAnyResource theSourceResource,
364                        MdmTransactionContext theMdmTransactionContext) {
365                List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theSourceResource);
366                if (!eidFromResource.isEmpty()) {
367                        updateGoldenResourceExternalEidFromSourceResource(
368                                        theGoldenResource, theSourceResource, theMdmTransactionContext);
369                }
370        }
371
372        /**
373         * Clones the specified canonical EID into the identifier field on the resource
374         *
375         * @param theFhirContext         Context to pull resource definitions from
376         * @param theResourceToCloneInto Resource to set the EID on
377         * @param theEid                 EID to be set
378         */
379        public void cloneEidIntoResource(
380                        FhirContext theFhirContext, IBaseResource theResourceToCloneInto, CanonicalEID theEid) {
381                // get a ref to the actual ID Field
382                RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceToCloneInto);
383                // hapi has 2 metamodels: for children and types
384                BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER);
385                ca.uhn.fhir.util.TerserUtil.cloneIdentifierIntoResource(
386                                theFhirContext,
387                                resourceIdentifier,
388                                IdentifierUtil.toId(theFhirContext, theEid),
389                                theResourceToCloneInto);
390        }
391}