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