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