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