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