001/*-
002 * #%L
003 * HAPI FHIR - Core Library
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.util;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
027import ca.uhn.fhir.context.RuntimeResourceDefinition;
028import ca.uhn.fhir.i18n.Msg;
029import ca.uhn.fhir.rest.api.EncodingEnum;
030import jakarta.annotation.Nonnull;
031import org.apache.commons.lang3.Strings;
032import org.hl7.fhir.instance.model.api.IBase;
033import org.hl7.fhir.instance.model.api.IBaseBundle;
034import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.hl7.fhir.instance.model.api.IPrimitiveType;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import java.lang.reflect.Method;
041import java.util.ArrayList;
042import java.util.List;
043import java.util.Objects;
044import java.util.Optional;
045import java.util.function.Predicate;
046
047public class ResourceUtil {
048
049        private static final String ENCODING = "ENCODING_TYPE";
050        private static final String RAW_ = "RAW_";
051        private static final String EQUALS_DEEP = "equalsDeep";
052        public static final String DATA_ABSENT_REASON_EXTENSION_URI =
053                        "http://hl7.org/fhir/StructureDefinition/data-absent-reason";
054
055        private static final Logger ourLog = LoggerFactory.getLogger(ResourceUtil.class);
056
057        /**
058         * A strategy object that specifies which rules to apply when merging <code>Coding</code>
059         * and <code>CodeableConcept</code> fields
060         */
061        public static class MergeControlParameters {
062                private boolean myIgnoreCodeableConceptCodingOrder;
063                private boolean myMergeCodings;
064                private boolean myMergeCodingDetails;
065
066                public boolean isIgnoreCodeableConceptCodingOrder() {
067                        return myIgnoreCodeableConceptCodingOrder;
068                }
069
070                /**
071                 * In most cases, the order of elements in a FHIR list should not be considered meaningful. This parameter
072                 * specifies whether the order of <code>Coding</code> entities within a <code>CodeableConcept</code>
073                 * matters when performing a merge.
074                 * @param theIgnoreCodeableConceptCodingOrder if true, two <code>CodeableConcept</code> entities will
075                 *                                            be considered to match each other if they contain
076                 *                                            matching <code>Coding</code>s in any order. If false,
077                 *                                            the <code>Coding</code>s must be in the same order.
078                 */
079                public void setIgnoreCodeableConceptCodingOrder(boolean theIgnoreCodeableConceptCodingOrder) {
080                        myIgnoreCodeableConceptCodingOrder = theIgnoreCodeableConceptCodingOrder;
081                }
082
083                public boolean isMergeCodings() {
084                        return myMergeCodings;
085                }
086
087                /**
088                 * If the <code>Coding</code>s of one <code>CodeableConcept</code> are a strict subset of another, the two
089                 * may be considered to match, and can be merged into a single element that contains the larger list of
090                 * <code>Coding</code>s. The case where the two lists of <code>Coding</code>s overlap, but each contains
091                 * elements that are absent from the other, the two <code>CodeableConcept</code>s will be considered distinct,
092                 * and will not be merged.
093                 * @param theMergeCodings if true, two <code>CodeableConcept</code> entities will be considered to match each
094                 *                        other if all the <code>Coding</code>s from the shorter list occur in the longer list.
095                 *                        If false, the lists must be the same length, and contain exactly the same elements.
096                 */
097                public void setMergeCodings(boolean theMergeCodings) {
098                        myMergeCodings = theMergeCodings;
099                }
100
101                public boolean isMergeCodingDetails() {
102                        return myMergeCodingDetails;
103                }
104
105                /**
106                 * Two <code>Coding</code>s may be considered to match if they share the same business key (system, code).
107                 * If this is the case, the remaining fields of the element can be merged into a survivor <code>Coding</code>.
108                 * @param theMergeCodingDetails if true, will match on the <code>Coding</code> business key only.
109                 *                              if false, matching requires exact equality of every field.
110                 */
111                public void setMergeCodingDetails(boolean theMergeCodingDetails) {
112                        myMergeCodingDetails = theMergeCodingDetails;
113                }
114        }
115
116        private ResourceUtil() {}
117
118        /**
119         * Exclusion predicate for keeping all fields.
120         */
121        public static final Predicate<String> INCLUDE_ALL = s -> true;
122
123        /**
124         * This method removes the narrative from the resource, or if the resource is a bundle, removes the narrative from
125         * all of the resources in the bundle
126         *
127         * @param theContext The fhir context
128         * @param theInput   The resource to remove the narrative from
129         */
130        public static void removeNarrative(FhirContext theContext, IBaseResource theInput) {
131                if (theInput instanceof IBaseBundle) {
132                        for (IBaseResource next : BundleUtil.toListOfResources(theContext, (IBaseBundle) theInput)) {
133                                removeNarrative(theContext, next);
134                        }
135                }
136
137                BaseRuntimeElementCompositeDefinition<?> element = theContext.getResourceDefinition(theInput.getClass());
138                BaseRuntimeChildDefinition textElement = element.getChildByName("text");
139                if (textElement != null) {
140                        textElement.getMutator().setValue(theInput, null);
141                }
142        }
143
144        public static void addRawDataToResource(
145                        @Nonnull IBaseResource theResource, @Nonnull EncodingEnum theEncodingType, String theSerializedData) {
146                theResource.setUserData(getRawUserDataKey(theEncodingType), theSerializedData);
147                theResource.setUserData(ENCODING, theEncodingType);
148        }
149
150        public static EncodingEnum getEncodingTypeFromUserData(@Nonnull IBaseResource theResource) {
151                return (EncodingEnum) theResource.getUserData(ENCODING);
152        }
153
154        public static String getRawStringFromResourceOrNull(@Nonnull IBaseResource theResource) {
155                EncodingEnum type = (EncodingEnum) theResource.getUserData(ENCODING);
156                if (type != null) {
157                        return (String) theResource.getUserData(getRawUserDataKey(type));
158                }
159                return null;
160        }
161
162        private static String getRawUserDataKey(EncodingEnum theEncodingEnum) {
163                return RAW_ + theEncodingEnum.name();
164        }
165
166        /**
167         * Merges all fields on the provided instance. <code>theTarget</code> will contain a union of all values from
168         * <code>theSource</code> instance and <code>theTarget</code> instance.
169         *
170         * @param theFhirContext Context holding resource definition
171         * @param theSource      The FHIR element to merge the fields from
172         * @param theTarget      The FHIR element to merge the fields into
173         */
174        public static void mergeAllFields(FhirContext theFhirContext, IBase theSource, IBase theTarget) {
175                mergeAllFields(theFhirContext, theSource, theTarget, new MergeControlParameters());
176        }
177
178        /**
179         * Merges all fields on the provided instance. <code>theTarget</code> will contain a union of all values from
180         * <code>theSource</code> instance and <code>theTarget</code> instance.
181         *
182         * @param theFhirContext            Context holding resource definition
183         * @param theSource                 The FHIR element to merge the fields from
184         * @param theTarget                 The FHIR element to merge the fields into
185         * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge
186         */
187        public static void mergeAllFields(
188                        FhirContext theFhirContext,
189                        IBase theSource,
190                        IBase theTarget,
191                        MergeControlParameters theMergeControlParameters) {
192                mergeFields(theFhirContext, theSource, theTarget, INCLUDE_ALL, theMergeControlParameters);
193        }
194
195        /**
196         * Merges values of all field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields
197         * values are compared via the equalsDeep method, or via object identity if this method is not available.
198         *
199         * @param theFhirContext       Context holding resource definition
200         * @param theSource            Resource to merge the specified field from
201         * @param theTarget            Resource to merge the specified field into
202         * @param theInclusionStrategy Predicate to test which fields should be merged
203         */
204        public static void mergeFields(
205                        FhirContext theFhirContext, IBase theSource, IBase theTarget, Predicate<String> theInclusionStrategy) {
206                mergeFields(theFhirContext, theSource, theTarget, theInclusionStrategy, new MergeControlParameters());
207        }
208
209        /**
210         * Merges values of all field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields
211         * with type <code>Coding</code> or <code>CodeableConcept</code> will be recursively merged according to the
212         * strategy specified by <code>theMergeControlParameters</code>. Fields of other types
213         * are compared via the equalsDeep method, or via object identity if this method is not available.
214         *
215         * @param theFhirContext            Context holding resource definition
216         * @param theSource                 Resource to merge the specified field from
217         * @param theTarget                 Resource to merge the specified field into
218         * @param theInclusionStrategy      Predicate to test which fields should be merged
219         * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge
220         */
221        public static void mergeFields(
222                        FhirContext theFhirContext,
223                        IBase theSource,
224                        IBase theTarget,
225                        Predicate<String> theInclusionStrategy,
226                        MergeControlParameters theMergeControlParameters) {
227                BaseRuntimeElementDefinition<?> definition = theFhirContext.getElementDefinition(theSource.getClass());
228                if (definition instanceof BaseRuntimeElementCompositeDefinition<?> compositeDefinition) {
229                        for (BaseRuntimeChildDefinition childDefinition : compositeDefinition.getChildrenAndExtension()) {
230                                if (!theInclusionStrategy.test(childDefinition.getElementName())) {
231                                        continue;
232                                }
233
234                                List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theSource);
235                                List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTarget);
236
237                                mergeFields(
238                                                theFhirContext,
239                                                theTarget,
240                                                childDefinition,
241                                                theFromFieldValues,
242                                                theToFieldValues,
243                                                theMergeControlParameters);
244                        }
245                }
246        }
247
248        /**
249         * Merges value of the specified field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields
250         * values are compared via the equalsDeep method, or via object identity if this method is not available.
251         *
252         * @param theFhirContext Context holding resource definition
253         * @param theFieldName   Name of the child filed to merge
254         * @param theSource      Resource to merge the specified field from
255         * @param theTarget      Resource to merge the specified field into
256         */
257        public static void mergeField(
258                        FhirContext theFhirContext, String theFieldName, IBaseResource theSource, IBaseResource theTarget) {
259                mergeField(theFhirContext, theFieldName, theSource, theTarget, new MergeControlParameters());
260        }
261
262        /**
263         * Merges value of the specified field from <code>theSource</code> resource to <code>theTarget</code> resource. Fields
264         * with type <code>Coding</code> or <code>CodeableConcept</code> will be recursively merged according to the
265         * strategy specified by <code>theMergeControlParameters</code>. Fields of other types
266         * are compared via the equalsDeep method, or via object identity if this method is not available.
267         *
268         * @param theFhirContext            Context holding resource definition
269         * @param theFieldName              Name of the child filed to merge
270         * @param theSource                 Resource to merge the specified field from
271         * @param theTarget                 Resource to merge the specified field into
272         * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge
273         */
274        public static void mergeField(
275                        FhirContext theFhirContext,
276                        String theFieldName,
277                        IBaseResource theSource,
278                        IBaseResource theTarget,
279                        MergeControlParameters theMergeControlParameters) {
280                BaseRuntimeChildDefinition childDefinition =
281                                getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theSource);
282
283                List<IBase> theFromFieldValues = childDefinition.getAccessor().getValues(theSource);
284                List<IBase> theToFieldValues = childDefinition.getAccessor().getValues(theTarget);
285
286                mergeFields(
287                                theFhirContext,
288                                theTarget,
289                                childDefinition,
290                                theFromFieldValues,
291                                theToFieldValues,
292                                theMergeControlParameters);
293        }
294
295        private static void mergeFields(
296                        FhirContext theFhirContext,
297                        IBase theTarget,
298                        BaseRuntimeChildDefinition theChildDefinition,
299                        List<IBase> theSourceFieldValues,
300                        List<IBase> theTargetFieldValues,
301                        MergeControlParameters theMergeControlParameters) {
302                FhirTerser terser = theFhirContext.newTerser();
303
304                if (!theSourceFieldValues.isEmpty()
305                                && theTargetFieldValues.stream().anyMatch(ResourceUtil::hasDataAbsentReason)) {
306                        // If the target resource has a data absent reason, and there is potentially real data incoming
307                        // in the source resource, we should clear the data absent reason because it won't be absent anymore.
308                        theTargetFieldValues = removeDataAbsentReason(theTarget, theChildDefinition, theTargetFieldValues);
309                }
310
311                List<IBase> filteredFromFieldValues = filterValuesThatAlreadyExistInTarget(
312                                terser, theSourceFieldValues, theTargetFieldValues, theMergeControlParameters);
313
314                for (IBase fromFieldValue : filteredFromFieldValues) {
315                        IBase newFieldValue;
316                        if (Strings.CI.equals(fromFieldValue.fhirType(), "CodeableConcept")) {
317                                newFieldValue = mergeOrClone(
318                                                theFhirContext,
319                                                theChildDefinition,
320                                                fromFieldValue,
321                                                theTargetFieldValues,
322                                                theMergeControlParameters,
323                                                targetValue -> isCodeableConceptMergeCandidate(
324                                                                fromFieldValue, targetValue, terser, theMergeControlParameters));
325                        } else if (Strings.CI.equals(fromFieldValue.fhirType(), "Coding")) {
326                                newFieldValue = mergeOrClone(
327                                                theFhirContext,
328                                                theChildDefinition,
329                                                fromFieldValue,
330                                                theTargetFieldValues,
331                                                theMergeControlParameters,
332                                                targetValue ->
333                                                                isCodingMergeCandidate(terser, fromFieldValue, targetValue, theMergeControlParameters));
334                        } else {
335                                newFieldValue = createNewElement(terser, theChildDefinition, fromFieldValue);
336                        }
337
338                        if (newFieldValue != null) {
339                                try {
340                                        theTargetFieldValues.add(newFieldValue);
341                                } catch (UnsupportedOperationException e) {
342                                        theChildDefinition.getMutator().setValue(theTarget, newFieldValue);
343                                        theTargetFieldValues = theChildDefinition.getAccessor().getValues(theTarget);
344                                }
345                        }
346                }
347        }
348
349        /**
350         * If <code>theMergePredicate</code> identifies one of the elements in <code>theTargetFieldValues</code>
351         * as a match for <code>theSourceFieldValue</code>, those two elements will be merged.
352         * Otherwise, <code>theSourceFieldValue</code> will be cloned and returned.
353         * @param theFhirContext            Context holding resource definition
354         * @param theChildDefinition        The definition of the field being merged
355         * @param theSourceFieldValue       A value from the source resource
356         * @param theTargetFieldValues      The values already present in the target resource
357         * @param theMergeControlParameters Parameters to provide fine-grained control over the behaviour of the merge
358         * @param theMergePredicate         An algorithm to identify matching resources
359         * @return a clone of <code>theSourceFieldValue</code> if none of the values in <code>theTargetFieldValues</code>
360         *         is a match. Otherwise, null, indicating that a merge took place
361         */
362        private static IBase mergeOrClone(
363                        FhirContext theFhirContext,
364                        BaseRuntimeChildDefinition theChildDefinition,
365                        IBase theSourceFieldValue,
366                        List<IBase> theTargetFieldValues,
367                        MergeControlParameters theMergeControlParameters,
368                        Predicate<IBase> theMergePredicate) {
369                IBase newFieldValue = null;
370                FhirTerser terser = theFhirContext.newTerser();
371                Optional<IBase> matchedTargetValue =
372                                theTargetFieldValues.stream().filter(theMergePredicate).findFirst();
373                if (matchedTargetValue.isPresent()) {
374                        mergeAllFields(theFhirContext, theSourceFieldValue, matchedTargetValue.get(), theMergeControlParameters);
375                } else {
376                        newFieldValue = createNewElement(terser, theChildDefinition, theSourceFieldValue);
377                }
378                return newFieldValue;
379        }
380
381        private static IBase createNewElement(
382                        FhirTerser theTerser, BaseRuntimeChildDefinition theChildDefinition, IBase theFromFieldValue) {
383                IBase newFieldValue = newElement(theTerser, theChildDefinition, theFromFieldValue);
384                if (theFromFieldValue instanceof IPrimitiveType) {
385                        try {
386                                Method copyMethod = getMethod(theFromFieldValue, "copy");
387                                if (copyMethod != null) {
388                                        newFieldValue = (IBase) copyMethod.invoke(theFromFieldValue);
389                                }
390                        } catch (Exception t) {
391                                ((IPrimitiveType<?>) newFieldValue)
392                                                .setValueAsString(((IPrimitiveType<?>) theFromFieldValue).getValueAsString());
393                        }
394                } else {
395                        theTerser.cloneInto(theFromFieldValue, newFieldValue, true);
396                }
397                return newFieldValue;
398        }
399
400        private static List<IBase> filterValuesThatAlreadyExistInTarget(
401                        FhirTerser theTerser,
402                        List<IBase> theFromFieldValues,
403                        List<IBase> theToFieldValues,
404                        MergeControlParameters theMergeControlParameters) {
405                List<IBase> filteredFromFieldValues = new ArrayList<>();
406                for (IBase fromFieldValue : theFromFieldValues) {
407                        if (theToFieldValues.isEmpty()) {
408                                // if the target field is unpopulated, accept any value from the source field
409                                filteredFromFieldValues.add(fromFieldValue);
410                        } else if (!hasDataAbsentReason(fromFieldValue)) {
411                                // if the value from the source field does not have a data absent reason extension,
412                                // evaluate its suitability for inclusion
413                                if (Strings.CI.equals(fromFieldValue.fhirType(), "codeableConcept")) {
414                                        if (!containsCodeableConcept(
415                                                        fromFieldValue, theToFieldValues, theTerser, theMergeControlParameters)) {
416                                                filteredFromFieldValues.add(fromFieldValue);
417                                        }
418                                } else if (!contains(fromFieldValue, theToFieldValues)) {
419                                        // include it if the target list doesn't already contain an exact match
420                                        filteredFromFieldValues.add(fromFieldValue);
421                                }
422                        }
423                }
424                return filteredFromFieldValues;
425        }
426
427        private static BaseRuntimeChildDefinition getBaseRuntimeChildDefinition(
428                        FhirContext theFhirContext, String theFieldName, IBaseResource theFrom) {
429                RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom);
430                BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theFieldName);
431                Objects.requireNonNull(childDefinition);
432                return childDefinition;
433        }
434
435        private static Method getMethod(IBase theBase, String theMethodName) {
436                Method method = null;
437                for (Method m : theBase.getClass().getDeclaredMethods()) {
438                        if (m.getName().equals(theMethodName)) {
439                                method = m;
440                                break;
441                        }
442                }
443                return method;
444        }
445
446        private static boolean evaluateEquality(IBase theItem1, IBase theItem2, Method theMethod) {
447                if (theMethod != null) {
448                        try {
449                                return (Boolean) theMethod.invoke(theItem1, theItem2);
450                        } catch (Exception e) {
451                                ourLog.debug("{} Unable to compare equality via {}", Msg.code(2821), theMethod.getName(), e);
452                        }
453                }
454                return theItem1.equals(theItem2);
455        }
456
457        private static boolean contains(IBase theItem, List<IBase> theItems) {
458                final Method method = getMethod(theItem, EQUALS_DEEP);
459                return theItems.stream().anyMatch(i -> evaluateEquality(i, theItem, method));
460        }
461
462        /**
463         * Evaluates whether a given source CodeableConcept can be merged into a target
464         * CodeableConcept
465         *
466         * @param theSourceItem             the source item
467         * @param theTargetItem             the target item
468         * @param theTerser                 a terser for introspecting the items
469         * @param theMergeControlParameters parameters providing fine-grained control over the merge operation
470         * @return true if the source item can be merged into the target item
471         */
472        private static boolean isCodeableConceptMergeCandidate(
473                        IBase theSourceItem,
474                        IBase theTargetItem,
475                        FhirTerser theTerser,
476                        MergeControlParameters theMergeControlParameters) {
477                // First, compare the shallow fields of the CodeableConcepts.
478                Method shallowEquals = getMethod(theSourceItem, "equalsShallow");
479                boolean isMergeCandidate = evaluateEquality(theSourceItem, theTargetItem, shallowEquals);
480
481                // if the shallow fields match, we proceed to compare the lists of Codings
482                if (theMergeControlParameters.isIgnoreCodeableConceptCodingOrder()) {
483                        isMergeCandidate &=
484                                        isCodingListsMatchUnordered(theSourceItem, theTargetItem, theTerser, theMergeControlParameters);
485                } else {
486                        isMergeCandidate &= isCodingListsMatchOrdered(
487                                        theSourceItem, theTargetItem, theTerser, theMergeControlParameters, isMergeCandidate);
488                }
489
490                return isMergeCandidate;
491        }
492
493        private static boolean isCodingListsMatchOrdered(
494                        IBase theSourceItem,
495                        IBase theTargetItem,
496                        FhirTerser theTerser,
497                        MergeControlParameters theMergeControlParameters,
498                        boolean isMergeCandidate) {
499                List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding");
500                List<IBase> targetCodings = theTerser.getValues(theTargetItem, "coding");
501                if (theMergeControlParameters.isMergeCodings()) {
502                        int prefixLength = Math.min(sourceCodings.size(), targetCodings.size());
503                        for (int i = 0; i < prefixLength; i++) {
504                                isMergeCandidate &= isCodingMergeCandidate(
505                                                theTerser, sourceCodings.get(i), targetCodings.get(i), theMergeControlParameters);
506                        }
507                } else {
508                        if (sourceCodings.size() == targetCodings.size()) {
509                                for (int i = 0; i < sourceCodings.size(); i++) {
510                                        isMergeCandidate &= isCodingMergeCandidate(
511                                                        theTerser, sourceCodings.get(i), targetCodings.get(i), theMergeControlParameters);
512                                }
513                        } else {
514                                isMergeCandidate = false;
515                        }
516                }
517                return isMergeCandidate;
518        }
519
520        private static boolean isCodingListsMatchUnordered(
521                        IBase theSourceItem,
522                        IBase theTargetItem,
523                        FhirTerser theTerser,
524                        MergeControlParameters theMergeControlParameters) {
525                boolean isMergeCandidate;
526                List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding");
527                List<IBase> targetCodings = theTerser.getValues(theTargetItem, "coding");
528                if (theMergeControlParameters.isMergeCodings()) {
529                        if (sourceCodings.size() < targetCodings.size()) {
530                                isMergeCandidate = sourceCodings.stream().allMatch(sourceCoding -> targetCodings.stream()
531                                                .anyMatch(targetCoding -> isCodingMergeCandidate(
532                                                                theTerser, sourceCoding, targetCoding, theMergeControlParameters)));
533                        } else {
534                                isMergeCandidate = targetCodings.stream().allMatch(targetCoding -> sourceCodings.stream()
535                                                .anyMatch(sourceCoding -> isCodingMergeCandidate(
536                                                                theTerser, sourceCoding, targetCoding, theMergeControlParameters)));
537                        }
538                } else {
539                        isMergeCandidate = sourceCodings.size() == targetCodings.size()
540                                        && sourceCodings.stream().allMatch(sourceCoding -> targetCodings.stream()
541                                                        .anyMatch(targetCoding -> isCodingMergeCandidate(
542                                                                        theTerser, sourceCoding, targetCoding, theMergeControlParameters)));
543                }
544                return isMergeCandidate;
545        }
546
547        @SuppressWarnings("rawtypes")
548        private static boolean isCodingMergeCandidate(
549                        FhirTerser theTerser,
550                        IBase theSourceCoding,
551                        IBase theTargetCoding,
552                        MergeControlParameters theMergeControlParameters) {
553                boolean codingMatches;
554                if (theMergeControlParameters.isMergeCodingDetails()) {
555                        // Use the tuple (system,code) as a business key on Coding
556                        Optional<IPrimitiveType> sourceSystem =
557                                        theTerser.getSingleValue(theSourceCoding, "system", IPrimitiveType.class);
558                        Optional<IPrimitiveType> sourceCode =
559                                        theTerser.getSingleValue(theSourceCoding, "code", IPrimitiveType.class);
560                        Optional<IPrimitiveType> targetSystem =
561                                        theTerser.getSingleValue(theTargetCoding, "system", IPrimitiveType.class);
562                        Optional<IPrimitiveType> targetCode =
563                                        theTerser.getSingleValue(theTargetCoding, "code", IPrimitiveType.class);
564                        boolean systemMatches = sourceSystem.isPresent()
565                                        && targetSystem.isPresent()
566                                        && Strings.CS.equals(
567                                                        sourceSystem.get().getValueAsString(),
568                                                        targetSystem.get().getValueAsString());
569                        boolean codeMatches = sourceCode.isPresent()
570                                        && targetCode.isPresent()
571                                        && Strings.CS.equals(
572                                                        sourceCode.get().getValueAsString(),
573                                                        targetCode.get().getValueAsString());
574                        codingMatches = systemMatches && codeMatches;
575                } else {
576                        // require an exact match on every field
577                        Method deepEquals = getMethod(theSourceCoding, EQUALS_DEEP);
578                        codingMatches = evaluateEquality(theSourceCoding, theTargetCoding, deepEquals);
579                }
580                return codingMatches;
581        }
582
583        /**
584         * Evaluates whether a list of target CodeableConcepts already contains a given source
585         * CodeableConcept. The order of Codings may or may not matter, depending on the
586         * configuration parameters, but otherwise we evaluate equivalence in the strictest
587         * available sense, since values filtered out by this method will not be candidates
588         * for subsequent merge operations.
589         *
590         * @param theSourceItem             The source value
591         * @param theTargetItems            The list of target values
592         * @param theTerser                 A terser to use to inspect the values
593         * @param theMergeControlParameters A set of parameters to control the operation
594         * @return true if the source item already exists in the list of target items
595         */
596        private static boolean containsCodeableConcept(
597                        IBase theSourceItem,
598                        List<IBase> theTargetItems,
599                        FhirTerser theTerser,
600                        MergeControlParameters theMergeControlParameters) {
601                Method shallowEquals = getMethod(theSourceItem, "equalsShallow");
602                List<IBase> shallowMatches = theTargetItems.stream()
603                                .filter(targetItem -> evaluateEquality(targetItem, theSourceItem, shallowEquals))
604                                .toList();
605
606                if (theMergeControlParameters.isIgnoreCodeableConceptCodingOrder()) {
607                        return shallowMatches.stream().anyMatch(targetItem -> {
608                                List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding");
609                                List<IBase> targetCodings = theTerser.getValues(targetItem, "coding");
610                                return sourceCodings.stream().allMatch(sourceCoding -> {
611                                        Method deepEquals = getMethod(sourceCoding, EQUALS_DEEP);
612                                        return targetCodings.stream()
613                                                        .anyMatch(targetCoding -> evaluateEquality(sourceCoding, targetCoding, deepEquals));
614                                });
615                        });
616                } else {
617                        return shallowMatches.stream().anyMatch(targetItem -> {
618                                boolean match = true;
619                                List<IBase> sourceCodings = theTerser.getValues(theSourceItem, "coding");
620                                List<IBase> targetCodings = theTerser.getValues(targetItem, "coding");
621                                if (sourceCodings.size() == targetCodings.size()) {
622                                        for (int i = 0; i < sourceCodings.size(); i++) {
623                                                Method deepEquals = getMethod(sourceCodings.get(i), EQUALS_DEEP);
624                                                match &= evaluateEquality(sourceCodings.get(i), targetCodings.get(i), deepEquals);
625                                        }
626                                } else {
627                                        match = false;
628                                }
629                                return match;
630                        });
631                }
632        }
633
634        private static boolean hasDataAbsentReason(IBase theItem) {
635                if (theItem instanceof IBaseHasExtensions hasExtensions) {
636                        return hasExtensions.getExtension().stream()
637                                        .anyMatch(t -> Strings.CS.equals(t.getUrl(), DATA_ABSENT_REASON_EXTENSION_URI));
638                }
639                return false;
640        }
641
642        private static List<IBase> removeDataAbsentReason(
643                        IBase theFhirElement, BaseRuntimeChildDefinition theFieldDefinition, List<IBase> theFieldValues) {
644                for (int i = 0; i < theFieldValues.size(); i++) {
645                        if (hasDataAbsentReason(theFieldValues.get(i))) {
646                                try {
647                                        theFieldDefinition.getMutator().remove(theFhirElement, i);
648                                } catch (UnsupportedOperationException e) {
649                                        // the field must be single-valued, just clear it
650                                        theFieldDefinition.getMutator().setValue(theFhirElement, null);
651                                }
652                        }
653                }
654                return theFieldDefinition.getAccessor().getValues(theFhirElement);
655        }
656
657        /**
658         * Creates a new element taking into consideration elements with choice that are not directly retrievable by element
659         * name
660         *
661         * @param theFhirTerser      A terser instance for the FHIR release
662         * @param theChildDefinition Child to create a new instance for
663         * @param theFromFieldValue  The base parent field
664         * @return Returns the new element with the given value if configured
665         */
666        private static IBase newElement(
667                        FhirTerser theFhirTerser, BaseRuntimeChildDefinition theChildDefinition, IBase theFromFieldValue) {
668                BaseRuntimeElementDefinition<?> runtimeElementDefinition;
669                if (theChildDefinition instanceof RuntimeChildChoiceDefinition) {
670                        runtimeElementDefinition =
671                                        theChildDefinition.getChildElementDefinitionByDatatype(theFromFieldValue.getClass());
672                } else {
673                        runtimeElementDefinition = theChildDefinition.getChildByName(theChildDefinition.getElementName());
674                }
675                if ("contained".equals(runtimeElementDefinition.getName())) {
676                        IBaseResource sourceResource = (IBaseResource) theFromFieldValue;
677                        return theFhirTerser.clone(sourceResource);
678                } else {
679                        return runtimeElementDefinition.newInstance();
680                }
681        }
682}