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.i18n.Msg;
027import ca.uhn.fhir.parser.path.EncodeContextPath;
028import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
029import ca.uhn.fhir.util.IModelVisitor2;
030import ca.uhn.fhir.util.ParametersUtil;
031import jakarta.annotation.Nonnull;
032import jakarta.annotation.Nullable;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.lang3.Validate;
035import org.hl7.fhir.instance.model.api.IBase;
036import org.hl7.fhir.instance.model.api.IBaseEnumeration;
037import org.hl7.fhir.instance.model.api.IBaseParameters;
038import org.hl7.fhir.instance.model.api.IBaseResource;
039import org.hl7.fhir.instance.model.api.IIdType;
040import org.hl7.fhir.instance.model.api.IPrimitiveType;
041import org.hl7.fhir.utilities.xhtml.XhtmlNode;
042
043import java.util.ArrayList;
044import java.util.Collections;
045import java.util.HashSet;
046import java.util.List;
047import java.util.Objects;
048import java.util.Optional;
049import java.util.Set;
050
051import static java.util.Objects.isNull;
052import static org.apache.commons.lang3.StringUtils.defaultString;
053
054public class FhirPatch {
055
056        public static final String OPERATION_ADD = "add";
057        public static final String OPERATION_DELETE = "delete";
058        public static final String OPERATION_INSERT = "insert";
059        public static final String OPERATION_MOVE = "move";
060        public static final String OPERATION_REPLACE = "replace";
061        public static final String PARAMETER_DESTINATION = "destination";
062        public static final String PARAMETER_INDEX = "index";
063        public static final String PARAMETER_NAME = "name";
064        public static final String PARAMETER_OPERATION = "operation";
065        public static final String PARAMETER_PATH = "path";
066        public static final String PARAMETER_SOURCE = "source";
067        public static final String PARAMETER_TYPE = "type";
068        public static final String PARAMETER_VALUE = "value";
069
070        private final FhirContext myContext;
071        private boolean myIncludePreviousValueInDiff;
072        private Set<EncodeContextPath> myIgnorePaths = Collections.emptySet();
073
074        public FhirPatch(FhirContext theContext) {
075                myContext = theContext;
076        }
077
078        /**
079         * Adds a path element that will not be included in generated diffs. Values can take the form
080         * <code>ResourceName.fieldName.fieldName</code> and wildcards are supported, such
081         * as <code>*.meta</code>.
082         */
083        public void addIgnorePath(String theIgnorePath) {
084                Validate.notBlank(theIgnorePath, "theIgnorePath must not be null or empty");
085
086                if (myIgnorePaths.isEmpty()) {
087                        myIgnorePaths = new HashSet<>();
088                }
089                myIgnorePaths.add(new EncodeContextPath(theIgnorePath));
090        }
091
092        public void setIncludePreviousValueInDiff(boolean theIncludePreviousValueInDiff) {
093                myIncludePreviousValueInDiff = theIncludePreviousValueInDiff;
094        }
095
096        public void apply(IBaseResource theResource, IBaseResource thePatch) {
097
098                List<IBase> opParameters = ParametersUtil.getNamedParameters(myContext, thePatch, PARAMETER_OPERATION);
099                for (IBase nextOperation : opParameters) {
100                        String type = ParametersUtil.getParameterPartValueAsString(myContext, nextOperation, PARAMETER_TYPE);
101                        type = defaultString(type);
102
103                        if (OPERATION_DELETE.equals(type)) {
104                                handleDeleteOperation(theResource, nextOperation);
105                        } else if (OPERATION_ADD.equals(type)) {
106                                handleAddOperation(theResource, nextOperation);
107                        } else if (OPERATION_REPLACE.equals(type)) {
108                                handleReplaceOperation(theResource, nextOperation);
109                        } else if (OPERATION_INSERT.equals(type)) {
110                                handleInsertOperation(theResource, nextOperation);
111                        } else if (OPERATION_MOVE.equals(type)) {
112                                handleMoveOperation(theResource, nextOperation);
113                        } else {
114                                throw new InvalidRequestException(Msg.code(1267) + "Unknown patch operation type: " + type);
115                        }
116                }
117        }
118
119        private void handleAddOperation(IBaseResource theResource, IBase theParameters) {
120
121                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
122                String elementName = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_NAME);
123
124                String containingPath = defaultString(path);
125
126                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
127                for (IBase nextElement : containingElements) {
128                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
129
130                        IBase newValue = getNewValue(theParameters, childDefinition);
131
132                        childDefinition.getChildDef().getMutator().addValue(nextElement, newValue);
133                }
134        }
135
136        private void handleInsertOperation(IBaseResource theResource, IBase theParameters) {
137
138                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
139                path = defaultString(path);
140
141                int lastDot = path.lastIndexOf(".");
142                String containingPath = path.substring(0, lastDot);
143                String elementName = path.substring(lastDot + 1);
144                Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_INDEX)
145                                .orElseThrow(() -> new InvalidRequestException("No index supplied for insert operation"));
146
147                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
148                for (IBase nextElement : containingElements) {
149
150                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
151
152                        IBase newValue = getNewValue(theParameters, childDefinition);
153
154                        List<IBase> existingValues =
155                                        new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement));
156                        if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) {
157                                String msg = myContext
158                                                .getLocalizer()
159                                                .getMessage(FhirPatch.class, "invalidInsertIndex", insertIndex, path, existingValues.size());
160                                throw new InvalidRequestException(Msg.code(1270) + msg);
161                        }
162                        existingValues.add(insertIndex, newValue);
163
164                        childDefinition.getChildDef().getMutator().setValue(nextElement, null);
165                        for (IBase nextNewValue : existingValues) {
166                                childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue);
167                        }
168                }
169        }
170
171        private void handleDeleteOperation(IBaseResource theResource, IBase theParameters) {
172
173                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
174                path = defaultString(path);
175
176                ParsedPath parsedPath = ParsedPath.parse(path);
177                List<IBase> containingElements = myContext
178                                .newFhirPath()
179                                .evaluate(
180                                                theResource,
181                                                parsedPath.getEndsWithAFilterOrIndex() ? parsedPath.getContainingPath() : path,
182                                                IBase.class);
183
184                for (IBase nextElement : containingElements) {
185                        if (parsedPath.getEndsWithAFilterOrIndex()) {
186                                // if the path ends with a filter or index, we must be dealing with a list
187                                deleteFromList(theResource, nextElement, parsedPath.getLastElementName(), path);
188                        } else {
189                                deleteSingleElement(nextElement);
190                        }
191                }
192        }
193
194        private void deleteFromList(
195                        IBaseResource theResource,
196                        IBase theContainingElement,
197                        String theListElementName,
198                        String theElementToDeletePath) {
199                ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName);
200
201                List<IBase> existingValues =
202                                new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(theContainingElement));
203                List<IBase> elementsToRemove =
204                                myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class);
205                existingValues.removeAll(elementsToRemove);
206
207                childDefinition.getChildDef().getMutator().setValue(theContainingElement, null);
208                for (IBase nextNewValue : existingValues) {
209                        childDefinition.getChildDef().getMutator().addValue(theContainingElement, nextNewValue);
210                }
211        }
212
213        private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) {
214                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
215                path = defaultString(path);
216
217                ParsedPath parsedPath = ParsedPath.parse(path);
218
219                List<IBase> containingElements =
220                                myContext.newFhirPath().evaluate(theResource, parsedPath.getContainingPath(), IBase.class);
221                for (IBase containingElement : containingElements) {
222
223                        ChildDefinition childDefinition = findChildDefinition(containingElement, parsedPath.getLastElementName());
224                        IBase newValue = getNewValue(theParameters, childDefinition);
225                        if (parsedPath.getEndsWithAFilterOrIndex()) {
226                                // if the path ends with a filter or index, we must be dealing with a list
227                                replaceInList(newValue, theResource, containingElement, childDefinition, path);
228                        } else {
229                                childDefinition.getChildDef().getMutator().setValue(containingElement, newValue);
230                        }
231                }
232        }
233
234        private void replaceInList(
235                        IBase theNewValue,
236                        IBaseResource theResource,
237                        IBase theContainingElement,
238                        ChildDefinition theChildDefinitionForTheList,
239                        String theFullReplacePath) {
240
241                List<IBase> existingValues = new ArrayList<>(
242                                theChildDefinitionForTheList.getChildDef().getAccessor().getValues(theContainingElement));
243                List<IBase> valuesToReplace = myContext.newFhirPath().evaluate(theResource, theFullReplacePath, IBase.class);
244                if (valuesToReplace.isEmpty()) {
245                        String msg = myContext
246                                        .getLocalizer()
247                                        .getMessage(FhirPatch.class, "noMatchingElementForPath", theFullReplacePath);
248                        throw new InvalidRequestException(Msg.code(2617) + msg);
249                }
250
251                BaseRuntimeChildDefinition.IMutator listMutator =
252                                theChildDefinitionForTheList.getChildDef().getMutator();
253                // clear the whole list first, then reconstruct it in the loop below replacing the values that need to be
254                // replaced
255                listMutator.setValue(theContainingElement, null);
256                for (IBase existingValue : existingValues) {
257                        if (valuesToReplace.contains(existingValue)) {
258                                listMutator.addValue(theContainingElement, theNewValue);
259                        } else {
260                                listMutator.addValue(theContainingElement, existingValue);
261                        }
262                }
263        }
264
265        private void handleMoveOperation(IBaseResource theResource, IBase theParameters) {
266                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
267                path = defaultString(path);
268
269                int lastDot = path.lastIndexOf(".");
270                String containingPath = path.substring(0, lastDot);
271                String elementName = path.substring(lastDot + 1);
272                Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(
273                                                myContext, theParameters, PARAMETER_DESTINATION)
274                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
275                Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE)
276                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
277
278                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
279                for (IBase nextElement : containingElements) {
280
281                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
282
283                        List<IBase> existingValues =
284                                        new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement));
285                        if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) {
286                                String msg = myContext
287                                                .getLocalizer()
288                                                .getMessage(
289                                                                FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size());
290                                throw new InvalidRequestException(Msg.code(1268) + msg);
291                        }
292                        IBase newValue = existingValues.remove(removeIndex.intValue());
293
294                        if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) {
295                                String msg = myContext
296                                                .getLocalizer()
297                                                .getMessage(
298                                                                FhirPatch.class,
299                                                                "invalidMoveDestinationIndex",
300                                                                insertIndex,
301                                                                path,
302                                                                existingValues.size());
303                                throw new InvalidRequestException(Msg.code(1269) + msg);
304                        }
305                        existingValues.add(insertIndex, newValue);
306
307                        childDefinition.getChildDef().getMutator().setValue(nextElement, null);
308                        for (IBase nextNewValue : existingValues) {
309                                childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue);
310                        }
311                }
312        }
313
314        private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) {
315                BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass());
316
317                String childName = theElementName;
318                BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName);
319                BaseRuntimeElementDefinition<?> childElement;
320                if (childDef == null) {
321                        childName = theElementName + "[x]";
322                        childDef = elementDef.getChildByName(childName);
323                        childElement = childDef.getChildByName(
324                                        childDef.getValidChildNames().iterator().next());
325                } else {
326                        childElement = childDef.getChildByName(childName);
327                }
328
329                return new ChildDefinition(childDef, childElement);
330        }
331
332        private IBase getNewValue(IBase theParameters, ChildDefinition theChildDefinition) {
333                Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE);
334                Optional<IBase> valuePartValue =
335                                ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE);
336
337                IBase newValue;
338                if (valuePartValue.isPresent()) {
339                        newValue = maybeMassageToEnumeration(valuePartValue.get(), theChildDefinition);
340
341                } else {
342                        List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList());
343
344                        newValue = createAndPopulateNewElement(theChildDefinition, partParts);
345                }
346
347                return newValue;
348        }
349
350        private IBase maybeMassageToEnumeration(IBase theValue, ChildDefinition theChildDefinition) {
351                IBase retVal = theValue;
352
353                if (IBaseEnumeration.class.isAssignableFrom(
354                                                theChildDefinition.getChildElement().getImplementingClass())
355                                || XhtmlNode.class.isAssignableFrom(
356                                                theChildDefinition.getChildElement().getImplementingClass())) {
357                        // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition
358                        // to build one, since
359                        // it needs the right factory object passed to its constructor
360                        IPrimitiveType<?> newValueInstance;
361                        if (theChildDefinition.getChildDef().getInstanceConstructorArguments() != null) {
362                                newValueInstance = (IPrimitiveType<?>) theChildDefinition
363                                                .getChildElement()
364                                                .newInstance(theChildDefinition.getChildDef().getInstanceConstructorArguments());
365                        } else {
366                                newValueInstance =
367                                                (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance();
368                        }
369                        newValueInstance.setValueAsString(((IPrimitiveType<?>) theValue).getValueAsString());
370                        retVal = newValueInstance;
371                }
372
373                return retVal;
374        }
375
376        @Nonnull
377        private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) {
378                return myContext.newTerser().getValues(theParametersParameterComponent, "part");
379        }
380
381        /**
382         * this method will instantiate an element according to the provided Definition and initialize its fields
383         * from the values provided in thePartParts.  a part usually represent a datatype as a name/value[X] pair.
384         * it may also represent a complex type like an Extension.
385         *
386         * @param theDefinition wrapper around the runtime definition of the element to be populated
387         * @param thePartParts list of Part to populate the element that will be created from theDefinition
388         * @return an element that was created from theDefinition and populated with the parts
389         */
390        private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) {
391                IBase newElement = theDefinition.getChildElement().newInstance();
392
393                for (IBase nextValuePartPart : thePartParts) {
394
395                        String name = myContext
396                                        .newTerser()
397                                        .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class)
398                                        .map(IPrimitiveType::getValueAsString)
399                                        .orElse(null);
400
401                        if (StringUtils.isBlank(name)) {
402                                continue;
403                        }
404
405                        Optional<IBase> optionalValue =
406                                        myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class);
407
408                        if (optionalValue.isPresent()) {
409                                // we have a dataType. let's extract its value and assign it.
410
411                                ChildDefinition childDefinition = findChildDefinition(newElement, name);
412                                IBase newValue = maybeMassageToEnumeration(optionalValue.get(), childDefinition);
413
414                                BaseRuntimeChildDefinition partChildDef =
415                                                theDefinition.getChildElement().getChildByName(name);
416
417                                if (isNull(partChildDef)) {
418                                        name = name + "[x]";
419                                        partChildDef = theDefinition.getChildElement().getChildByName(name);
420                                }
421
422                                partChildDef.getMutator().setValue(newElement, newValue);
423
424                                // a part represent a datatype or a complexType but not both at the same time.
425                                continue;
426                        }
427
428                        List<IBase> part = extractPartsFromPart(nextValuePartPart);
429
430                        if (!part.isEmpty()) {
431                                // we have a complexType.  let's find its definition and recursively process
432                                // them till all complexTypes are processed.
433                                ChildDefinition childDefinition = findChildDefinition(newElement, name);
434
435                                IBase childNewValue = createAndPopulateNewElement(childDefinition, part);
436
437                                childDefinition.getChildDef().getMutator().setValue(newElement, childNewValue);
438                        }
439                }
440
441                return newElement;
442        }
443
444        private void deleteSingleElement(IBase theElementToDelete) {
445                myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() {
446                        @Override
447                        public boolean acceptElement(
448                                        IBase theElement,
449                                        List<IBase> theContainingElementPath,
450                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
451                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
452                                if (theElement instanceof IPrimitiveType) {
453                                        ((IPrimitiveType<?>) theElement).setValueAsString(null);
454                                }
455                                return true;
456                        }
457                });
458        }
459
460        public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) {
461                IBaseParameters retVal = ParametersUtil.newInstance(myContext);
462                String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName();
463
464                if (theOldValue == null) {
465
466                        IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION);
467                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
468                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName);
469                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue);
470
471                } else {
472
473                        String oldValueTypeName =
474                                        myContext.getResourceDefinition(theOldValue).getName();
475                        Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type");
476
477                        BaseRuntimeElementCompositeDefinition<?> def =
478                                        myContext.getResourceDefinition(theOldValue).getBaseDefinition();
479                        String path = def.getName();
480
481                        EncodeContextPath contextPath = new EncodeContextPath();
482                        contextPath.pushPath(path, true);
483
484                        compare(retVal, contextPath, def, path, path, theOldValue, theNewValue);
485
486                        contextPath.popPath();
487                        assert contextPath.getPath().isEmpty();
488                }
489
490                return retVal;
491        }
492
493        private void compare(
494                        IBaseParameters theDiff,
495                        EncodeContextPath theSourceEncodeContext,
496                        BaseRuntimeElementDefinition<?> theDef,
497                        String theSourcePath,
498                        String theTargetPath,
499                        IBase theOldField,
500                        IBase theNewField) {
501
502                boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext);
503                if (pathIsIgnored) {
504                        return;
505                }
506
507                BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass());
508                BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass());
509                if (!sourceDef.getName().equals(targetDef.getName())) {
510                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
511                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
512                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
513                        addValueToDiff(operation, theOldField, theNewField);
514                } else {
515                        if (theOldField instanceof IPrimitiveType) {
516                                IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField;
517                                IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField;
518                                String oldValueAsString = toValue(oldPrimitive);
519                                String newValueAsString = toValue(newPrimitive);
520                                if (!Objects.equals(oldValueAsString, newValueAsString)) {
521                                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
522                                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
523                                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
524                                        addValueToDiff(operation, oldPrimitive, newPrimitive);
525                                }
526                        }
527
528                        List<BaseRuntimeChildDefinition> children = theDef.getChildren();
529                        for (BaseRuntimeChildDefinition nextChild : children) {
530                                compareField(
531                                                theDiff,
532                                                theSourceEncodeContext,
533                                                theSourcePath,
534                                                theTargetPath,
535                                                theOldField,
536                                                theNewField,
537                                                nextChild);
538                        }
539                }
540        }
541
542        private void compareField(
543                        IBaseParameters theDiff,
544                        EncodeContextPath theSourceEncodePath,
545                        String theSourcePath,
546                        String theTargetPath,
547                        IBase theOldField,
548                        IBase theNewField,
549                        BaseRuntimeChildDefinition theChildDef) {
550                String elementName = theChildDef.getElementName();
551                boolean repeatable = theChildDef.getMax() != 1;
552                theSourceEncodePath.pushPath(elementName, false);
553                if (pathIsIgnored(theSourceEncodePath)) {
554                        theSourceEncodePath.popPath();
555                        return;
556                }
557
558                List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField);
559                List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField);
560
561                int sourceIndex = 0;
562                int targetIndex = 0;
563                while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) {
564
565                        IBase sourceChildField = sourceValues.get(sourceIndex);
566                        Validate.notNull(sourceChildField); // not expected to happen, but just in case
567                        BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass());
568                        IBase targetChildField = targetValues.get(targetIndex);
569                        Validate.notNull(targetChildField); // not expected to happen, but just in case
570                        String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : "");
571                        String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "");
572
573                        compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField);
574
575                        sourceIndex++;
576                        targetIndex++;
577                }
578
579                // Find newly inserted items
580                while (targetIndex < targetValues.size()) {
581                        String path = theTargetPath + "." + elementName;
582                        addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef);
583                        targetIndex++;
584                }
585
586                // Find deleted items
587                while (sourceIndex < sourceValues.size()) {
588                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
589                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE);
590                        ParametersUtil.addPartString(
591                                        myContext,
592                                        operation,
593                                        PARAMETER_PATH,
594                                        theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""));
595
596                        sourceIndex++;
597                        targetIndex++;
598                }
599
600                theSourceEncodePath.popPath();
601        }
602
603        private void addInsertItems(
604                        IBaseParameters theDiff,
605                        List<? extends IBase> theTargetValues,
606                        int theTargetIndex,
607                        String thePath,
608                        BaseRuntimeChildDefinition theChildDefinition) {
609                IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
610                ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
611                ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath);
612                ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex);
613
614                IBase value = theTargetValues.get(theTargetIndex);
615                BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass());
616
617                /*
618                 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover
619                 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't
620                 * just put it into part.value because it isn't an actual type. So we have to put all of its
621                 * children in instead.
622                 */
623                if (valueDef.isStandardType()) {
624                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value);
625                } else {
626                        for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) {
627                                List<IBase> childValues = nextChild.getAccessor().getValues(value);
628                                for (int index = 0; index < childValues.size(); index++) {
629                                        boolean childRepeatable = theChildDefinition.getMax() != 1;
630                                        String elementName = nextChild.getChildNameByDatatype(
631                                                        childValues.get(index).getClass());
632                                        String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName;
633                                        addInsertItems(theDiff, childValues, index, targetPath, nextChild);
634                                }
635                        }
636                }
637        }
638
639        private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) {
640
641                if (myIncludePreviousValueInDiff) {
642                        IBase oldValue = massageValueForDiff(theOldValue);
643                        ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue);
644                }
645
646                IBase newValue = massageValueForDiff(theNewValue);
647                ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue);
648        }
649
650        private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) {
651                boolean pathIsIgnored = false;
652                for (EncodeContextPath next : myIgnorePaths) {
653                        if (theSourceEncodeContext.startsWith(next, false)) {
654                                pathIsIgnored = true;
655                                break;
656                        }
657                }
658                return pathIsIgnored;
659        }
660
661        private IBase massageValueForDiff(IBase theNewValue) {
662                IBase massagedValue = theNewValue;
663
664                // XHTML content is dealt with by putting it in a string
665                if (theNewValue instanceof XhtmlNode) {
666                        String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString();
667                        massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString);
668                }
669
670                // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs
671                if (theNewValue instanceof IIdType) {
672                        String idPart = ((IIdType) theNewValue).getIdPart();
673                        massagedValue = myContext.getElementDefinition("id").newInstance(idPart);
674                }
675
676                return massagedValue;
677        }
678
679        private String toValue(IPrimitiveType<?> theOldPrimitive) {
680                if (theOldPrimitive instanceof IIdType) {
681                        return ((IIdType) theOldPrimitive).getIdPart();
682                }
683                return theOldPrimitive.getValueAsString();
684        }
685
686        private static class ChildDefinition {
687                private final BaseRuntimeChildDefinition myChildDef;
688                private final BaseRuntimeElementDefinition<?> myChildElement;
689
690                public ChildDefinition(
691                                BaseRuntimeChildDefinition theChildDef, BaseRuntimeElementDefinition<?> theChildElement) {
692                        this.myChildDef = theChildDef;
693                        this.myChildElement = theChildElement;
694                }
695
696                public BaseRuntimeChildDefinition getChildDef() {
697                        return myChildDef;
698                }
699
700                public BaseRuntimeElementDefinition<?> getChildElement() {
701                        return myChildElement;
702                }
703        }
704
705        /**
706         * This class helps parse a FHIR path into its component parts for easier patch operation processing.
707         * It has 3 components:
708         *  - The last element name, which is the last element in the path (not including any list index or filter)
709         *  - The containing path, which is the prefix of the path up to the last element
710         *  - A flag indicating whether the path has a filter or index on the last element of the path, which indicates
711         *  that the path we are dealing is probably for a list element.
712         * Examples:
713         * 1. For path "Patient.identifier[2].system",
714         *   - the lastElementName is "system",
715         *   - the containingPath is "Patient.identifier[2]",
716         *   - and endsWithAFilterOrIndex flag is false
717         *
718         *  2. For path "Patient.identifier[2]" or for path "Patient.identifier.where('system'='sys1')"
719         *  - the lastElementName is "identifier",
720         *  - the containingPath is "Patient",
721         *  - and the endsWithAFilterOrIndex is true
722         */
723        protected static class ParsedPath {
724                private final String myLastElementName;
725                private final String myContainingPath;
726                private final boolean myEndsWithAFilterOrIndex;
727
728                public ParsedPath(String theLastElementName, String theContainingPath, boolean theEndsWithAFilterOrIndex) {
729                        myLastElementName = theLastElementName;
730                        myContainingPath = theContainingPath;
731                        myEndsWithAFilterOrIndex = theEndsWithAFilterOrIndex;
732                }
733
734                /**
735                 * returns the last element of the path
736                 */
737                public String getLastElementName() {
738                        return myLastElementName;
739                }
740
741                /**
742                 * Returns the prefix of the path up to the last FHIR resource element
743                 */
744                public String getContainingPath() {
745                        return myContainingPath;
746                }
747
748                /**
749                 * Returns whether the path has a filter or index on the last element of the path, which indicates
750                 * that the path we are dealing is probably a list element.
751                 */
752                public boolean getEndsWithAFilterOrIndex() {
753                        return myEndsWithAFilterOrIndex;
754                }
755
756                public static ParsedPath parse(String path) {
757                        String containingPath;
758                        String elementName;
759                        boolean endsWithAFilterOrIndex = false;
760
761                        if (path.endsWith(")")) {
762                                // This is probably a filter, so we're probably dealing with a list
763                                endsWithAFilterOrIndex = true;
764                                int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses
765                                int lastDotIndex = path.lastIndexOf(
766                                                '.',
767                                                filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that
768                                int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1);
769                                containingPath = path.substring(0, secondLastDotIndex);
770                                elementName = path.substring(secondLastDotIndex + 1, lastDotIndex);
771                        } else if (path.endsWith("]")) {
772                                // This is almost definitely a list
773                                endsWithAFilterOrIndex = true;
774                                int openBracketIndex = path.lastIndexOf('[');
775                                int lastDotIndex = path.lastIndexOf('.', openBracketIndex);
776                                containingPath = path.substring(0, lastDotIndex);
777                                elementName = path.substring(lastDotIndex + 1, openBracketIndex);
778                        } else {
779                                int lastDot = path.lastIndexOf(".");
780                                containingPath = path.substring(0, lastDot);
781                                elementName = path.substring(lastDot + 1);
782                        }
783
784                        return new ParsedPath(elementName, containingPath, endsWithAFilterOrIndex);
785                }
786        }
787}