001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.jpa.patch;
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.fhirpath.IFhirPath;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.jpa.util.FhirPathUtils;
029import ca.uhn.fhir.parser.path.EncodeContextPath;
030import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
031import ca.uhn.fhir.util.IModelVisitor2;
032import ca.uhn.fhir.util.ParametersUtil;
033import jakarta.annotation.Nonnull;
034import jakarta.annotation.Nullable;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.commons.lang3.Validate;
037import org.hl7.fhir.instance.model.api.IBase;
038import org.hl7.fhir.instance.model.api.IBaseEnumeration;
039import org.hl7.fhir.instance.model.api.IBaseParameters;
040import org.hl7.fhir.instance.model.api.IBaseResource;
041import org.hl7.fhir.instance.model.api.IIdType;
042import org.hl7.fhir.instance.model.api.IPrimitiveType;
043import org.hl7.fhir.utilities.xhtml.XhtmlNode;
044
045import java.util.ArrayList;
046import java.util.Collections;
047import java.util.HashSet;
048import java.util.List;
049import java.util.Objects;
050import java.util.Optional;
051import java.util.Set;
052import java.util.Stack;
053import java.util.concurrent.atomic.AtomicReference;
054import java.util.function.Predicate;
055
056import static java.util.Objects.isNull;
057import static org.apache.commons.lang3.StringUtils.defaultString;
058import static org.apache.commons.lang3.StringUtils.isNotBlank;
059
060/**
061 * FhirPatch handler.
062 * Patch is defined by the spec: https://www.hl7.org/fhir/R4/fhirpatch.html
063 */
064public class FhirPatch {
065        org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirPatch.class);
066
067        public static final String OPERATION_ADD = "add";
068        public static final String OPERATION_DELETE = "delete";
069        public static final String OPERATION_INSERT = "insert";
070        public static final String OPERATION_MOVE = "move";
071        public static final String OPERATION_REPLACE = "replace";
072        public static final String PARAMETER_DESTINATION = "destination";
073        public static final String PARAMETER_INDEX = "index";
074        public static final String PARAMETER_NAME = "name";
075        public static final String PARAMETER_OPERATION = "operation";
076        public static final String PARAMETER_PATH = "path";
077        public static final String PARAMETER_SOURCE = "source";
078        public static final String PARAMETER_TYPE = "type";
079        public static final String PARAMETER_VALUE = "value";
080
081        private final FhirContext myContext;
082        private boolean myIncludePreviousValueInDiff;
083        private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet();
084
085        public FhirPatch(FhirContext theContext) {
086                myContext = theContext;
087        }
088
089        /**
090         * Adds a path element that will not be included in generated diffs. Values can take the form
091         * <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such
092         * as <code>*.meta</code>.
093         */
094        public void addIgnorePath(String theIgnorePath) {
095                Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty");
096
097                if (myIgnorePaths.isEmpty()) {
098                        myIgnorePaths = new HashSet<>();
099                }
100                myIgnorePaths.add(new EncodeContextPath(theIgnorePath));
101        }
102
103        public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) {
104                myIncludePreviousValueInDiff = theIncludePreviousValueInDiff;
105        }
106
107        public void apply(IBaseResource theResource, IBaseResource thePatch) {
108
109                List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, PARAMETER_OPERATION);
110                for (IBase nextOperation : opParameters) {
111                        String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOperation, PARAMETER_TYPE);
112                        type = defaultString(type);
113
114                        if (OPERATION_DELETE.equals(type)) {
115                                handleDeleteOperation(theResource, nextOperation);
116                        } else if (OPERATION_ADD.equals(type)) {
117                                handleAddOperation(theResource, nextOperation);
118                        } else if (OPERATION_REPLACE.equals(type)) {
119                                handleReplaceOperation(theResource, nextOperation);
120                        } else if (OPERATION_INSERT.equals(type)) {
121                                handleInsertOperation(theResource, nextOperation);
122                        } else if (OPERATION_MOVE.equals(type)) {
123                                handleMoveOperation(theResource, nextOperation);
124                        } else {
125                                throw new InvalidRequestException(Msg.code(1267) + "Unknown patch operation type: " + type);
126                        }
127                }
128        }
129
130        private void handleAddOperation(IBaseResource theResource, IBase theParameters) {
131
132                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
133                String elementName = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_NAME);
134
135                String containingPath = defaultString(path);
136
137                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
138                for (IBase nextElement : containingElements) {
139                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
140
141                        IBase newValue = getNewValue(theParameters, childDefinition);
142
143                        childDefinition.getUseableChildDef().getMutator().addValue(nextElement, newValue);
144                }
145        }
146
147        private void handleInsertOperation(IBaseResource theResource, IBase theParameters) {
148
149                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
150                path = defaultString(path);
151
152                int lastDot = path.lastIndexOf(".");
153                String containingPath = path.substring(0, lastDot);
154                String elementName = path.substring(lastDot + 1);
155                Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_INDEX)
156                                .orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation"));
157
158                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
159                for (IBase nextElement : containingElements) {
160
161                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
162
163                        IBase newValue = getNewValue(theParameters, childDefinition);
164
165                        List<IBase> existingValues = new ArrayList<>(
166                                        childDefinition.getUseableChildDef().getAccessor().getValues(nextElement));
167                        if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) {
168                                String msg = myContext
169                                                .getLocalizer()
170                                                .getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size());
171                                throw new InvalidRequestException(Msg.code(1270) + msg);
172                        }
173                        existingValues.add(insertIndex, newValue);
174
175                        childDefinition.getUseableChildDef().getMutator().setValue(nextElement, null);
176                        for (IBase nextNewValue : existingValues) {
177                                childDefinition.getUseableChildDef().getMutator().addValue(nextElement, nextNewValue);
178                        }
179                }
180        }
181
182        private void handleDeleteOperation(IBaseResource theResource, IBase theParameters) {
183                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
184                path = defaultString(path);
185
186                ParsedPath parsedPath = ParsedPath.parse(path);
187                List<IBase> containingElements = myContext
188                                .newFhirPath()
189                                .evaluate(
190                                                theResource,
191                                                parsedPath.getEndsWithAFilterOrIndex() ? parsedPath.getContainingPath() : path,
192                                                IBase.class);
193
194                for (IBase nextElement : containingElements) {
195                        if (parsedPath.getEndsWithAFilterOrIndex()) {
196                                // if the path ends with a filter or index, we must be dealing with a list
197                                deleteFromList(theResource, nextElement, parsedPath.getLastElementName(), path);
198                        } else {
199                                deleteSingleElement(nextElement);
200                        }
201                }
202        }
203
204        private void deleteFromList(
205                        IBaseResource theResource,
206                        IBase theContainingElement,
207                        String theListElementName,
208                        String theElementToDeletePath) {
209                ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName);
210
211                List<IBase> existingValues = new ArrayList<>(
212                                childDefinition.getUseableChildDef().getAccessor().getValues(theContainingElement));
213                List<IBase> elementsToRemove =
214                                myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class);
215                existingValues.removeAll(elementsToRemove);
216
217                childDefinition.getUseableChildDef().getMutator().setValue(theContainingElement, null);
218                for (IBase nextNewValue : existingValues) {
219                        childDefinition.getUseableChildDef().getMutator().addValue(theContainingElement, nextNewValue);
220                }
221        }
222
223        private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) {
224                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
225                path = defaultString(path);
226
227                // TODO
228                /*
229                 * We should replace this with
230                 * IParsedExpression to expose the parsed parts of the
231                 * path (including functional nodes).
232                 *
233                 * Alternatively, could make an Antir parser using
234                 * the exposed grammar (http://hl7.org/fhirpath/N1/grammar.html)
235                 *
236                 * Might be required for more complex handling.
237                 */
238                IFhirPath fhirPath = myContext.newFhirPath();
239                try {
240                        IFhirPath.IParsedExpression exp = fhirPath.parse(path);
241                } catch (Exception theE) {
242                        throw new IllegalArgumentException(
243                                        Msg.code(2726) + String.format(" %s is not a valid fhir path", path), theE);
244                }
245                ParsedFhirPath parsedFhirPath = ParsedFhirPath.parse(path);
246                FhirPathChildDefinition parentDef = new FhirPathChildDefinition();
247
248                List<ParsedFhirPath.FhirPathNode> pathNodes = new ArrayList<>();
249                parsedFhirPath.getAllNodesWithPred(pathNodes, ParsedFhirPath.FhirPathNode::isNormalPathNode);
250                List<String> parts = new ArrayList<>();
251                for (ParsedFhirPath.FhirPathNode node : pathNodes) {
252                        parts.add(node.getValue());
253                }
254
255                // fetch all runtime definitions along fhirpath
256                FhirPathChildDefinition cd = childDefinition(parentDef, parts, theResource, fhirPath, parsedFhirPath, path);
257
258                // replace the value
259                replaceValuesByPath(cd, theParameters, fhirPath, parsedFhirPath);
260        }
261
262        private void replaceValuesByPath(
263                        FhirPathChildDefinition theChildDefinition,
264                        IBase theParameters,
265                        IFhirPath theFhirPath,
266                        ParsedFhirPath theParsedFhirPath) {
267                Optional<IBase> singleValuePart =
268                                ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE);
269                if (singleValuePart.isPresent()) {
270                        IBase replacementValue = singleValuePart.get();
271
272                        FhirPathChildDefinition childDefinitionToUse =
273                                        findChildDefinitionByReplacementType(theChildDefinition, replacementValue);
274
275                        // only a single replacement value (ie, not a replacement CompositeValue or anything)
276                        replaceSingleValue(theFhirPath, theParsedFhirPath, childDefinitionToUse, replacementValue);
277                        return; // guard
278                }
279
280                Optional<IBase> valueParts = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE);
281                if (valueParts.isPresent()) {
282                        // multiple replacement values provided via parts
283                        List<IBase> partParts = valueParts.map(this::extractPartsFromPart).orElse(Collections.emptyList());
284
285                        for (IBase nextValuePartPart : partParts) {
286                                String name = myContext
287                                                .newTerser()
288                                                .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class)
289                                                .map(IPrimitiveType::getValueAsString)
290                                                .orElse(null);
291
292                                if (StringUtils.isBlank(name)) {
293                                        continue;
294                                }
295
296                                Optional<IBase> optionalValue =
297                                                myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class);
298                                if (optionalValue.isPresent()) {
299                                        FhirPathChildDefinition childDefinitionToUse =
300                                                        findChildDefinitionAtEndOfPath(theChildDefinition, nextValuePartPart);
301
302                                        BaseRuntimeChildDefinition subChild =
303                                                        childDefinitionToUse.getElementDefinition().getChildByName(name);
304
305                                        subChild.getMutator().setValue(childDefinitionToUse.getBase(), optionalValue.get());
306                                }
307                        }
308
309                        return; // guard
310                }
311
312                // fall through to error state
313                throw new InvalidRequestException(Msg.code(2720) + " No valid replacement value for patch operation.");
314        }
315
316        private FhirPathChildDefinition findChildDefinitionByReplacementType(
317                        FhirPathChildDefinition theChildDefinition, IBase replacementValue) {
318                boolean isPrimitive = replacementValue instanceof IPrimitiveType<?>;
319                Predicate<FhirPathChildDefinition> predicate = def -> {
320                        if (isPrimitive) {
321                                // primitives will be at the very bottom (ie, no children underneath)
322                                return def.getBase() instanceof IPrimitiveType<?>;
323                        } else if (def.getBase().fhirType().equalsIgnoreCase(replacementValue.fhirType())) {
324                                return true;
325                        }
326                        return false;
327                };
328
329                return findChildDefinition(theChildDefinition, predicate);
330        }
331
332        private FhirPathChildDefinition findChildDefinitionAtEndOfPath(
333                        FhirPathChildDefinition theChildDefinition, IBase replacementValue) {
334                return findChildDefinition(theChildDefinition, childDefinition -> {
335                        return childDefinition.getChild() == null;
336                });
337        }
338
339        private FhirPathChildDefinition findChildDefinition(
340                        FhirPathChildDefinition theChildDefinition, Predicate<FhirPathChildDefinition> thePredicate) {
341                FhirPathChildDefinition childDefinitionToUse = theChildDefinition;
342                while (childDefinitionToUse != null) {
343                        if (thePredicate.test(childDefinitionToUse)) {
344                                return childDefinitionToUse;
345                        }
346                        childDefinitionToUse = childDefinitionToUse.getChild();
347                }
348
349                throw new InvalidRequestException(Msg.code(2719) + " No runtime definition found for patch operation.");
350        }
351
352        private void replaceSingleValue(
353                        IFhirPath theFhirPath,
354                        ParsedFhirPath theParsedFhirPath,
355                        FhirPathChildDefinition theTargetChildDefinition,
356                        IBase theReplacementValue) {
357                if (theTargetChildDefinition.getElementDefinition().getChildType()
358                                == BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE) {
359                        if (theTargetChildDefinition.getBase() instanceof IPrimitiveType<?> target
360                                        && theReplacementValue instanceof IPrimitiveType<?> source) {
361                                if (target.fhirType().equalsIgnoreCase(source.fhirType())) {
362                                        if (theTargetChildDefinition
363                                                                        .getParent()
364                                                                        .getBase()
365                                                                        .fhirType()
366                                                                        .equalsIgnoreCase("narrative")
367                                                        && theTargetChildDefinition.getFhirPath().equalsIgnoreCase("div")) {
368                                                /*
369                                                 * Special case handling for Narrative elements
370                                                 * because xhtml is a primitive type, but it's fhirtype is recorded as "string"
371                                                 * (which means we cannot actually assign it as a primitive type).
372                                                 *
373                                                 * Instead, we have to get the parent's type and set it's child as a new
374                                                 * XHTML child.
375                                                 */
376                                                FhirPathChildDefinition narrativeDefinition = theTargetChildDefinition.getParent();
377                                                BaseRuntimeElementDefinition<?> narrativeElement = narrativeDefinition.getElementDefinition();
378
379                                                BaseRuntimeElementDefinition<?> newXhtmlEl = myContext.getElementDefinition("xhtml");
380
381                                                IPrimitiveType<?> xhtmlType;
382                                                if (theTargetChildDefinition.getBaseRuntimeDefinition().getInstanceConstructorArguments()
383                                                                != null) {
384                                                        xhtmlType = (IPrimitiveType<?>) newXhtmlEl.newInstance(theTargetChildDefinition
385                                                                        .getBaseRuntimeDefinition()
386                                                                        .getInstanceConstructorArguments());
387                                                } else {
388                                                        xhtmlType = (IPrimitiveType<?>) newXhtmlEl.newInstance();
389                                                }
390
391                                                xhtmlType.setValueAsString(source.getValueAsString());
392                                                narrativeElement
393                                                                .getChildByName(theTargetChildDefinition.getFhirPath())
394                                                                .getMutator()
395                                                                .setValue(narrativeDefinition.getBase(), xhtmlType);
396                                        } else {
397                                                target.setValueAsString(source.getValueAsString());
398                                        }
399                                } else if (theTargetChildDefinition.getChild() != null) {
400                                        // there's subchildren (possibly we're setting an 'extension' value
401                                        FhirPathChildDefinition ct =
402                                                        findChildDefinitionAtEndOfPath(theTargetChildDefinition, theReplacementValue);
403                                        replaceSingleValue(theFhirPath, theParsedFhirPath, ct, theReplacementValue);
404                                } else {
405                                        if (theTargetChildDefinition.getBaseRuntimeDefinition() != null
406                                                        && !theTargetChildDefinition
407                                                                        .getBaseRuntimeDefinition()
408                                                                        .isMultipleCardinality()) {
409                                                // basic primitive type assignment
410                                                target.setValueAsString(source.getValueAsString());
411                                                return;
412                                        }
413
414                                        // the primitive can have multiple value types
415                                        BaseRuntimeElementDefinition<?> parentEl =
416                                                        theTargetChildDefinition.getParent().getElementDefinition();
417                                        String childFhirPath = theTargetChildDefinition.getFhirPath();
418
419                                        BaseRuntimeChildDefinition choiceTarget = parentEl.getChildByName(childFhirPath);
420                                        if (choiceTarget == null) {
421                                                // possibly a choice type
422                                                choiceTarget = parentEl.getChildByName(childFhirPath + "[x]");
423                                        }
424                                        choiceTarget
425                                                        .getMutator()
426                                                        .setValue(theTargetChildDefinition.getParent().getBase(), theReplacementValue);
427                                }
428                        }
429                        return;
430                }
431
432                IBase containingElement = theTargetChildDefinition.getParent().getBase();
433                BaseRuntimeChildDefinition runtimeDef = theTargetChildDefinition.getBaseRuntimeDefinition();
434                if (runtimeDef == null) {
435                        runtimeDef = theTargetChildDefinition.getParent().getBaseRuntimeDefinition();
436                }
437
438                if (runtimeDef.isMultipleCardinality()) {
439                        // a list
440                        List<IBase> existing = new ArrayList<>(runtimeDef.getAccessor().getValues(containingElement));
441                        if (existing.isEmpty()) {
442                                // no elements to replace - we shouldn't see this here though
443                                String msg = myContext
444                                                .getLocalizer()
445                                                .getMessage(FhirPatch.class, "noMatchingElementForPath", theParsedFhirPath.getRawPath());
446                                throw new InvalidRequestException(Msg.code(2617) + msg);
447                        }
448
449                        List<IBase> replaceables;
450                        if (FhirPathUtils.isSubsettingNode(theParsedFhirPath.getTail())) {
451                                replaceables = applySubsettingFilter(theParsedFhirPath, theParsedFhirPath.getTail(), existing);
452                        } else if (existing.size() == 1) {
453                                replaceables = existing;
454                        } else {
455                                String raw = theParsedFhirPath.getRawPath();
456                                String finalNode = theParsedFhirPath.getLastElementName();
457                                String subpath = raw.substring(raw.indexOf(finalNode));
458                                if (subpath.startsWith(finalNode) && subpath.length() > finalNode.length()) {
459                                        subpath = subpath.substring(finalNode.length() + 1); // + 1 for the "."
460                                }
461
462                                AtomicReference<String> subpathRef = new AtomicReference<>();
463                                subpathRef.set(subpath);
464                                replaceables = existing.stream()
465                                                .filter(item -> {
466                                                        Optional<IBase> matched = theFhirPath.evaluateFirst(item, subpathRef.get(), IBase.class);
467                                                        return matched.isPresent();
468                                                })
469                                                .toList();
470                        }
471
472                        if (replaceables.size() != 1) {
473                                throw new InvalidRequestException(
474                                                Msg.code(2715) + " Expected to find a single element, but provided FhirPath returned "
475                                                                + replaceables.size() + " elements.");
476                        }
477                        IBase valueToReplace = replaceables.get(0);
478
479                        BaseRuntimeChildDefinition.IMutator listMutator = runtimeDef.getMutator();
480                        // clear the whole list first, then reconstruct it in the loop below replacing the values that need to be
481                        // replaced
482                        listMutator.setValue(containingElement, null);
483                        for (IBase existingValue : existing) {
484                                if (valueToReplace.equals(existingValue)) {
485                                        listMutator.addValue(containingElement, theReplacementValue);
486                                } else {
487                                        listMutator.addValue(containingElement, existingValue);
488                                }
489                        }
490                } else {
491                        // a single element
492                        runtimeDef.getMutator().setValue(containingElement, theReplacementValue);
493                }
494        }
495
496        private List<IBase> applySubsettingFilter(
497                        ParsedFhirPath theParsed, ParsedFhirPath.FhirPathNode tail, List<IBase> filtered) {
498                if (tail.getListIndex() >= 0) {
499                        // specific index
500                        if (tail.getListIndex() < filtered.size()) {
501                                return List.of(filtered.get(tail.getListIndex()));
502                        } else {
503                                ourLog.info("Nothing matching index {}; nothing patched.", tail.getListIndex());
504                                return List.of();
505                        }
506                } else {
507                        if (filtered.isEmpty()) {
508                                // empty lists should match all filters, so we'll return it here
509                                ourLog.info("List contains no elements; no patching will occur");
510                                return List.of();
511                        }
512
513                        switch (tail.getValue()) {
514                                case "first" -> {
515                                        return List.of(filtered.get(0));
516                                }
517                                case "last" -> {
518                                        return List.of(filtered.get(filtered.size() - 1));
519                                }
520                                case "tail" -> {
521                                        if (filtered.size() == 1) {
522                                                ourLog.info("List contains only a single element - no patching will occur");
523                                                return List.of();
524                                        }
525                                        return filtered.subList(1, filtered.size());
526                                }
527                                case "single" -> {
528                                        if (filtered.size() != 1) {
529                                                throw new InvalidRequestException(
530                                                                Msg.code(2710) + " List contains more than a single element.");
531                                        }
532                                        // only one element
533                                        return filtered;
534                                }
535                                case "skip", "take" -> {
536                                        if (tail instanceof ParsedFhirPath.FhirPathFunction fn) {
537                                                String containedNum = fn.getContainedExp().getHead().getValue();
538                                                try {
539                                                        int num = Integer.parseInt(containedNum);
540
541                                                        if (tail.getValue().equals("skip")) {
542                                                                if (num < filtered.size()) {
543                                                                        return filtered.subList(num, filtered.size());
544                                                                }
545                                                        } else if (tail.getValue().equals("take")) {
546                                                                if (num < filtered.size()) {
547                                                                        return filtered.subList(0, num);
548                                                                } else {
549                                                                        // otherwise, return everything
550                                                                        return filtered;
551                                                                }
552                                                        }
553
554                                                        return List.of();
555                                                } catch (NumberFormatException ex) {
556                                                        ourLog.error("{} is not a number", containedNum, ex);
557                                                }
558                                        }
559                                        throw new InvalidRequestException(
560                                                        Msg.code(2712) + " Invalid fhir path element encountered: " + theParsed.getRawPath());
561                                }
562                                default -> {
563                                        // we shouldn't see this; it means we have not handled a filtering case
564                                        throw new InvalidRequestException(
565                                                        Msg.code(2711) + " Unrecognized filter of type " + tail.getValue());
566                                }
567                        }
568                }
569        }
570
571        private void throwNoElementsError(String theFullReplacePath) {
572                String msg =
573                                myContext.getLocalizer().getMessage(FhirPatch.class, "noMatchingElementForPath", theFullReplacePath);
574                throw new InvalidRequestException(Msg.code(2617) + msg);
575        }
576
577        private void handleMoveOperation(IBaseResource theResource, IBase theParameters) {
578                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
579                path = defaultString(path);
580
581                int lastDot = path.lastIndexOf(".");
582                String containingPath = path.substring(0, lastDot);
583                String elementName = path.substring(lastDot + 1);
584                Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(
585                                                myContext, theParameters, PARAMETER_DESTINATION)
586                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
587                Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE)
588                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
589
590                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
591                for (IBase nextElement : containingElements) {
592
593                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
594
595                        List<IBase> existingValues = new ArrayList<>(
596                                        childDefinition.getUseableChildDef().getAccessor().getValues(nextElement));
597                        if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) {
598                                String msg = myContext
599                                                .getLocalizer()
600                                                .getMessage(
601                                                                FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size());
602                                throw new InvalidRequestException(Msg.code(1268) + msg);
603                        }
604                        IBase newValue = existingValues.remove(removeIndex.intValue());
605
606                        if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) {
607                                String msg = myContext
608                                                .getLocalizer()
609                                                .getMessage(
610                                                                FhirPatch.class,
611                                                                "invalidMoveDestinationIndex",
612                                                                insertIndex,
613                                                                path,
614                                                                existingValues.size());
615                                throw new InvalidRequestException(Msg.code(1269) + msg);
616                        }
617                        existingValues.add(insertIndex, newValue);
618
619                        childDefinition.getUseableChildDef().getMutator().setValue(nextElement, null);
620                        for (IBase nextNewValue : existingValues) {
621                                childDefinition.getUseableChildDef().getMutator().addValue(nextElement, nextNewValue);
622                        }
623                }
624        }
625
626        private FhirPathChildDefinition childDefinition(
627                        FhirPathChildDefinition theParent,
628                        List<String> theFhirPathParts,
629                        IBase theBase,
630                        IFhirPath theFhirPath,
631                        ParsedFhirPath theParsedFhirPath,
632                        String theOriginalPath) {
633                FhirPathChildDefinition definition = new FhirPathChildDefinition();
634                definition.setBase(theBase); // set this IBase value
635                BaseRuntimeElementDefinition<?> parentElementDefinition = myContext.getElementDefinition(theBase.getClass());
636                definition.setElementDefinition(parentElementDefinition); // set this element
637
638                String head = theParsedFhirPath.getHead().getValue();
639                definition.setFhirPath(head);
640
641                if (theParent.getElementDefinition() != null) {
642                        definition.setBaseRuntimeDefinition(theParent.getElementDefinition().getChildByName(head));
643                }
644
645                String rawPath = theParsedFhirPath.getRawPath();
646
647                if (rawPath.equalsIgnoreCase(head)) {
648                        // we're at the bottom
649                        // return
650                        return definition;
651                }
652
653                // detach the head
654                String headVal = theFhirPathParts.remove(0);
655                String pathBeneathParent = rawPath.substring(headVal.length());
656                pathBeneathParent = FhirPathUtils.cleansePath(pathBeneathParent);
657
658                if (isNotBlank(pathBeneathParent) && !theFhirPathParts.isEmpty()) {
659                        Stack<ParsedFhirPath.FhirPathNode> filteringNodes = new Stack<>();
660
661                        String childFilteringPath = pathBeneathParent;
662                        String nextPath = pathBeneathParent;
663
664                        if (FhirPathUtils.isSubsettingNode(theParsedFhirPath.getTail())) {
665                                // the final node in this path is .first() or [0]... etc
666                                ParsedFhirPath.FhirPathNode filteringNode = theParsedFhirPath.getTail();
667                                filteringNodes.push(filteringNode);
668                                /*
669                                 * the field filtering path will be the path - tail value.
670                                 * This will also be nextPath (the one we recurse on)
671                                 */
672                                int endInd = pathBeneathParent.indexOf(filteringNode.getValue());
673                                if (endInd == -1) {
674                                        endInd = pathBeneathParent.length();
675                                }
676                                childFilteringPath = pathBeneathParent.substring(0, endInd);
677                                childFilteringPath = FhirPathUtils.cleansePath(childFilteringPath);
678                                nextPath = childFilteringPath;
679                        }
680
681                        String directChildName = theFhirPathParts.get(0);
682
683                        ParsedFhirPath newPath = ParsedFhirPath.parse(nextPath);
684
685                        if (newPath.getHead() instanceof ParsedFhirPath.FhirPathFunction fn && fn.hasContainedExp()) {
686                                newPath = fn.getContainedExp();
687
688                                childFilteringPath = newPath.getRawPath();
689                        }
690
691                        // get all direct children
692                        ParsedFhirPath.FhirPathNode newHead = newPath.getHead();
693                        List<IBase> allChildren = theFhirPath.evaluate(theBase, directChildName, IBase.class);
694
695                        // go through the children and take only the ones that match the path we have
696                        String filterPath = childFilteringPath;
697
698                        List<IBase> childs;
699                        if (filterPath.startsWith(newHead.getValue()) && !filterPath.equalsIgnoreCase(newHead.getValue())) {
700                                filterPath = filterPath.substring(newHead.getValue().length());
701                                filterPath = FhirPathUtils.cleansePath(filterPath);
702
703                                if (newPath.getHead().getNext() != null
704                                                && FhirPathUtils.isSubsettingNode(newPath.getHead().getNext())) {
705                                        // yet another filter node
706                                        ParsedFhirPath.FhirPathNode filterNode = newPath.getHead().getNext();
707                                        filteringNodes.push(filterNode);
708
709                                        String newRaw = newPath.getRawPath();
710                                        String updated = "";
711                                        if (filterNode.hasNext()) {
712                                                updated = newRaw.substring(
713                                                                newRaw.indexOf(filterNode.getNext().getValue()));
714                                                updated = FhirPathUtils.cleansePath(updated);
715                                        }
716                                        filterPath = updated;
717                                        updated = newPath.getHead().getValue() + "." + updated;
718                                        newPath = ParsedFhirPath.parse(updated);
719                                }
720                        }
721
722                        if (isNotBlank(filterPath)) {
723                                if (theFhirPathParts.contains(filterPath)) {
724                                        /*
725                                         * We're filtering on just a fhirpath node for some reason (ie, "identifier" or "reference" or "string").
726                                         *
727                                         * We don't need to apply the filter;
728                                         * all children should be the same as this filtered type
729                                         * (likely we have a subsetting filter to apply)
730                                         */
731                                        childs = allChildren;
732                                } else {
733                                        AtomicReference<String> ref = new AtomicReference<>();
734                                        ref.set(filterPath);
735                                        if (allChildren.size() > 1) {
736                                                childs = allChildren.stream()
737                                                                .filter(el -> {
738                                                                        Optional<IBase> match = theFhirPath.evaluateFirst(el, ref.get(), IBase.class);
739                                                                        return match.isPresent();
740                                                                })
741                                                                .toList();
742                                        } else {
743                                                // there is only 1 child (probably a top level element)
744                                                // we still filter own because child elements can have different child types (that might match
745                                                // multiple childs)
746                                                // eg: everything has "extension" on it
747                                                childs = allChildren.stream()
748                                                                .filter(el -> {
749                                                                        Optional<IBase> match = theFhirPath.evaluateFirst(el, ref.get(), IBase.class);
750                                                                        return match.isPresent();
751                                                                })
752                                                                .findFirst()
753                                                                .stream()
754                                                                .toList();
755                                        }
756                                }
757                        } else {
758                                childs = allChildren;
759                        }
760
761                        while (!filteringNodes.empty()) {
762                                ParsedFhirPath.FhirPathNode filteringNode = filteringNodes.pop();
763                                childs = applySubsettingFilter(newPath, filteringNode, childs);
764                        }
765
766                        // should only be one
767                        if (childs.size() != 1) {
768                                if (childs.isEmpty()) {
769                                        throwNoElementsError(theOriginalPath);
770                                }
771                                throw new InvalidRequestException(Msg.code(2704)
772                                                + " FhirPath returns more than 1 element. Patch cannot be done. " + theOriginalPath);
773                        }
774                        IBase child = childs.get(0);
775
776                        definition.setChild(
777                                        childDefinition(definition, theFhirPathParts, child, theFhirPath, newPath, theOriginalPath));
778                }
779
780                return definition;
781        }
782
783        private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) {
784                BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass());
785
786                String childName = theElementName;
787                BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName);
788                BaseRuntimeElementDefinition<?> childElement;
789                if (childDef == null) {
790                        childName = theElementName + "[x]";
791                        childDef = elementDef.getChildByName(childName);
792                        childElement = childDef.getChildByName(
793                                        childDef.getValidChildNames().iterator().next());
794                } else {
795                        childElement = childDef.getChildByName(childName);
796                }
797
798                return new ChildDefinition(childDef, childElement);
799        }
800
801        private IBase getNewValue(IBase theParameters, ChildDefinition theChildDefinition) {
802                Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE);
803                Optional<IBase> valuePartValue =
804                                ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE);
805
806                IBase newValue;
807                if (valuePartValue.isPresent()) {
808                        newValue = maybeMassageToEnumeration(valuePartValue.get(), theChildDefinition);
809
810                } else {
811                        List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList());
812
813                        newValue = createAndPopulateNewElement(theChildDefinition, partParts);
814                }
815
816                return newValue;
817        }
818
819        private IBase maybeMassageToEnumeration(IBase theValue, ChildDefinition theChildDefinition) {
820                IBase retVal = theValue;
821
822                if (IBaseEnumeration.class.isAssignableFrom(
823                                                theChildDefinition.getChildElement().getImplementingClass())
824                                || XhtmlNode.class.isAssignableFrom(
825                                                theChildDefinition.getChildElement().getImplementingClass())) {
826                        // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition
827                        // to build one, since
828                        // it needs the right factory object passed to its constructor
829                        IPrimitiveType<?> newValueInstance;
830                        if (theChildDefinition.getUseableChildDef().getInstanceConstructorArguments() != null) {
831                                newValueInstance = (IPrimitiveType<?>) theChildDefinition
832                                                .getChildElement()
833                                                .newInstance(theChildDefinition.getUseableChildDef().getInstanceConstructorArguments());
834                        } else {
835                                newValueInstance =
836                                                (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance();
837                        }
838                        newValueInstance.setValueAsString(((IPrimitiveType<?>) theValue).getValueAsString());
839                        retVal = newValueInstance;
840                }
841
842                return retVal;
843        }
844
845        @Nonnull
846        private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) {
847                return myContext.newTerser().getValues(theParametersParameterComponent, "part");
848        }
849
850        /**
851         * this method will instantiate an element according to the provided Definition and initialize its fields
852         * from the values provided in thePartParts.  a part usually represent a datatype as a name/value[X] pair.
853         * it may also represent a complex type like an Extension.
854         *
855         * @param theDefinition wrapper around the runtime definition of the element to be populated
856         * @param thePartParts list of Part to populate the element that will be created from theDefinition
857         * @return an element that was created from theDefinition and populated with the parts
858         */
859        private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) {
860                IBase newElement = theDefinition.getChildElement().newInstance();
861
862                for (IBase nextValuePartPart : thePartParts) {
863                        String name = myContext
864                                        .newTerser()
865                                        .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class)
866                                        .map(IPrimitiveType::getValueAsString)
867                                        .orElse(null);
868
869                        if (StringUtils.isBlank(name)) {
870                                continue;
871                        }
872
873                        Optional<IBase> optionalValue =
874                                        myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class);
875
876                        if (optionalValue.isPresent()) {
877                                // we have a dataType. let's extract its value and assign it.
878                                ChildDefinition childDefinition;
879                                childDefinition = findChildDefinition(newElement, name);
880
881                                IBase newValue = maybeMassageToEnumeration(optionalValue.get(), childDefinition);
882
883                                BaseRuntimeChildDefinition partChildDef =
884                                                theDefinition.getUsableChildElement().getChildByName(name);
885
886                                if (isNull(partChildDef)) {
887                                        name = name + "[x]";
888                                        partChildDef = theDefinition.getUsableChildElement().getChildByName(name);
889                                }
890
891                                partChildDef.getMutator().setValue(newElement, newValue);
892
893                                // a part represent a datatype or a complexType but not both at the same time.
894                                continue;
895                        }
896
897                        List<IBase> part = extractPartsFromPart(nextValuePartPart);
898
899                        if (!part.isEmpty()) {
900                                // we have a complexType.  let's find its definition and recursively process
901                                // them till all complexTypes are processed.
902                                ChildDefinition childDefinition = findChildDefinition(newElement, name);
903
904                                IBase childNewValue = createAndPopulateNewElement(childDefinition, part);
905
906                                childDefinition.getUseableChildDef().getMutator().setValue(newElement, childNewValue);
907                        }
908                }
909
910                return newElement;
911        }
912
913        private void deleteSingleElement(IBase theElementToDelete) {
914                myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() {
915                        @Override
916                        public boolean acceptElement(
917                                        IBase theElement,
918                                        List<IBase> theContainingElementPath,
919                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
920                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
921                                if (theElement instanceof IPrimitiveType) {
922                                        ((IPrimitiveType<?>) theElement).setValueAsString(null);
923                                }
924                                return true;
925                        }
926                });
927        }
928
929        public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) {
930                IBaseParameters retVal = ParametersUtil.newInstance(myContext);
931                String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName();
932
933                if (theOldValue == null) {
934                        IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION);
935                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
936                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName);
937                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue);
938                } else {
939                        String oldValueTypeName =
940                                        myContext.getResourceDefinition(theOldValue).getName();
941                        Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type");
942
943                        BaseRuntimeElementCompositeDefinition<?> def =
944                                        myContext.getResourceDefinition(theOldValue).getBaseDefinition();
945                        String path = def.getName();
946
947                        EncodeContextPath contextPath = new EncodeContextPath();
948                        contextPath.pushPath(path, true);
949
950                        compare(retVal, contextPath, def, path, path, theOldValue, theNewValue);
951
952                        contextPath.popPath();
953                        assert contextPath.getPath().isEmpty();
954                }
955
956                return retVal;
957        }
958
959        private void compare(
960                        IBaseParameters theDiff,
961                        EncodeContextPath theSourceEncodeContext,
962                        BaseRuntimeElementDefinition<?> theDef,
963                        String theSourcePath,
964                        String theTargetPath,
965                        IBase theOldField,
966                        IBase theNewField) {
967
968                boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext);
969                if (pathIsIgnored) {
970                        return;
971                }
972
973                BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass());
974                BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass());
975                if (!sourceDef.getName().equals(targetDef.getName())) {
976                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
977                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
978                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
979                        addValueToDiff(operation, theOldField, theNewField);
980                } else {
981                        if (theOldField instanceof IPrimitiveType) {
982                                IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField;
983                                IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField;
984                                String oldValueAsString = toValue(oldPrimitive);
985                                String newValueAsString = toValue(newPrimitive);
986                                if (!Objects.equals(oldValueAsString, newValueAsString)) {
987                                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
988                                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
989                                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
990                                        addValueToDiff(operation, oldPrimitive, newPrimitive);
991                                }
992                        }
993
994                        List<BaseRuntimeChildDefinition> children = theDef.getChildren();
995                        for (BaseRuntimeChildDefinition nextChild : children) {
996                                compareField(
997                                                theDiff,
998                                                theSourceEncodeContext,
999                                                theSourcePath,
1000                                                theTargetPath,
1001                                                theOldField,
1002                                                theNewField,
1003                                                nextChild);
1004                        }
1005                }
1006        }
1007
1008        private void compareField(
1009                        IBaseParameters theDiff,
1010                        EncodeContextPath theSourceEncodePath,
1011                        String theSourcePath,
1012                        String theTargetPath,
1013                        IBase theOldField,
1014                        IBase theNewField,
1015                        BaseRuntimeChildDefinition theChildDef) {
1016                String elementName = theChildDef.getElementName();
1017                boolean repeatable = theChildDef.getMax() != 1;
1018                theSourceEncodePath.pushPath(elementName, false);
1019                if (pathIsIgnored(theSourceEncodePath)) {
1020                        theSourceEncodePath.popPath();
1021                        return;
1022                }
1023
1024                List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField);
1025                List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField);
1026
1027                int sourceIndex = 0;
1028                int targetIndex = 0;
1029                while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) {
1030
1031                        IBase sourceChildField = sourceValues.get(sourceIndex);
1032                        Validate.notNull(sourceChildField); // not expected to happen, but just in case
1033                        BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass());
1034                        IBase targetChildField = targetValues.get(targetIndex);
1035                        Validate.notNull(targetChildField); // not expected to happen, but just in case
1036                        String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : "");
1037                        String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "");
1038
1039                        compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField);
1040
1041                        sourceIndex++;
1042                        targetIndex++;
1043                }
1044
1045                // Find newly inserted items
1046                while (targetIndex < targetValues.size()) {
1047                        String path = theTargetPath + "." + elementName;
1048                        addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef);
1049                        targetIndex++;
1050                }
1051
1052                // Find deleted items
1053                while (sourceIndex < sourceValues.size()) {
1054                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
1055                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE);
1056                        ParametersUtil.addPartString(
1057                                        myContext,
1058                                        operation,
1059                                        PARAMETER_PATH,
1060                                        theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""));
1061
1062                        sourceIndex++;
1063                        targetIndex++;
1064                }
1065
1066                theSourceEncodePath.popPath();
1067        }
1068
1069        private void addInsertItems(
1070                        IBaseParameters theDiff,
1071                        List<? extends IBase> theTargetValues,
1072                        int theTargetIndex,
1073                        String thePath,
1074                        BaseRuntimeChildDefinition theChildDefinition) {
1075                IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
1076                ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
1077                ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath);
1078                ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex);
1079
1080                IBase value = theTargetValues.get(theTargetIndex);
1081                BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass());
1082
1083                /*
1084                 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover
1085                 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't
1086                 * just put it into part.value because it isn't an actual type. So we have to put all of its
1087                 * children in instead.
1088                 */
1089                if (valueDef.isStandardType()) {
1090                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value);
1091                } else {
1092                        for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) {
1093                                List<IBase> childValues = nextChild.getAccessor().getValues(value);
1094                                for (int index = 0; index < childValues.size(); index++) {
1095                                        boolean childRepeatable = theChildDefinition.getMax() != 1;
1096                                        String elementName = nextChild.getChildNameByDatatype(
1097                                                        childValues.get(index).getClass());
1098                                        String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName;
1099                                        addInsertItems(theDiff, childValues, index, targetPath, nextChild);
1100                                }
1101                        }
1102                }
1103        }
1104
1105        private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) {
1106
1107                if (myIncludePreviousValueInDiff) {
1108                        IBase oldValue = massageValueForDiff(theOldValue);
1109                        ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue);
1110                }
1111
1112                IBase newValue = massageValueForDiff(theNewValue);
1113                ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue);
1114        }
1115
1116        private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) {
1117                boolean pathIsIgnored = false;
1118                for (EncodeContextPath next : myIgnorePaths) {
1119                        if (theSourceEncodeContext.startsWith(next, false)) {
1120                                pathIsIgnored = true;
1121                                break;
1122                        }
1123                }
1124                return pathIsIgnored;
1125        }
1126
1127        private IBase massageValueForDiff(IBase theNewValue) {
1128                IBase massagedValue = theNewValue;
1129
1130                // XHTML content is dealt with by putting it in a string
1131                if (theNewValue instanceof XhtmlNode) {
1132                        String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString();
1133                        massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString);
1134                }
1135
1136                // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs
1137                if (theNewValue instanceof IIdType) {
1138                        String idPart = ((IIdType) theNewValue).getIdPart();
1139                        massagedValue = myContext.getElementDefinition("id").newInstance(idPart);
1140                }
1141
1142                return massagedValue;
1143        }
1144
1145        private String toValue(IPrimitiveType<?> theOldPrimitive) {
1146                if (theOldPrimitive instanceof IIdType) {
1147                        return ((IIdType) theOldPrimitive).getIdPart();
1148                }
1149                return theOldPrimitive.getValueAsString();
1150        }
1151}