001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2024 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                String containingPath;
176                String elementName;
177
178                if (path.endsWith(")")) {
179                        // This is probably a filter, so we're probably dealing with a list
180                        int filterArgsIndex = path.lastIndexOf('('); // Let's hope there aren't nested parentheses
181                        int lastDotIndex = path.lastIndexOf(
182                                        '.', filterArgsIndex); // There might be a dot inside the parentheses, so look to the left of that
183                        int secondLastDotIndex = path.lastIndexOf('.', lastDotIndex - 1);
184                        containingPath = path.substring(0, secondLastDotIndex);
185                        elementName = path.substring(secondLastDotIndex + 1, lastDotIndex);
186                } else if (path.endsWith("]")) {
187                        // This is almost definitely a list
188                        int openBracketIndex = path.lastIndexOf('[');
189                        int lastDotIndex = path.lastIndexOf('.', openBracketIndex);
190                        containingPath = path.substring(0, lastDotIndex);
191                        elementName = path.substring(lastDotIndex + 1, openBracketIndex);
192                } else {
193                        containingPath = path;
194                        elementName = null;
195                }
196
197                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
198                for (IBase nextElement : containingElements) {
199                        if (elementName == null) {
200                                deleteSingleElement(nextElement);
201                        } else {
202                                deleteFromList(theResource, nextElement, elementName, path);
203                        }
204                }
205        }
206
207        private void deleteFromList(
208                        IBaseResource theResource,
209                        IBase theContainingElement,
210                        String theListElementName,
211                        String theElementToDeletePath) {
212                ChildDefinition childDefinition = findChildDefinition(theContainingElement, theListElementName);
213
214                List<IBase> existingValues =
215                                new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(theContainingElement));
216                List<IBase> elementsToRemove =
217                                myContext.newFhirPath().evaluate(theResource, theElementToDeletePath, IBase.class);
218                existingValues.removeAll(elementsToRemove);
219
220                childDefinition.getChildDef().getMutator().setValue(theContainingElement, null);
221                for (IBase nextNewValue : existingValues) {
222                        childDefinition.getChildDef().getMutator().addValue(theContainingElement, nextNewValue);
223                }
224        }
225
226        private void handleReplaceOperation(IBaseResource theResource, IBase theParameters) {
227                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
228                path = defaultString(path);
229
230                int lastDot = path.lastIndexOf(".");
231                String containingPath = path.substring(0, lastDot);
232                String elementName = path.substring(lastDot + 1);
233
234                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
235                for (IBase nextElement : containingElements) {
236
237                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
238
239                        IBase newValue = getNewValue(theParameters, nextElement, childDefinition);
240
241                        childDefinition.getChildDef().getMutator().setValue(nextElement, newValue);
242                }
243        }
244
245        private void handleMoveOperation(IBaseResource theResource, IBase theParameters) {
246                String path = ParametersUtil.getParameterPartValueAsString(myContext, theParameters, PARAMETER_PATH);
247                path = defaultString(path);
248
249                int lastDot = path.lastIndexOf(".");
250                String containingPath = path.substring(0, lastDot);
251                String elementName = path.substring(lastDot + 1);
252                Integer insertIndex = ParametersUtil.getParameterPartValueAsInteger(
253                                                myContext, theParameters, PARAMETER_DESTINATION)
254                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
255                Integer removeIndex = ParametersUtil.getParameterPartValueAsInteger(myContext, theParameters, PARAMETER_SOURCE)
256                                .orElseThrow(() -> new InvalidRequestException("No index supplied for move operation"));
257
258                List<IBase> containingElements = myContext.newFhirPath().evaluate(theResource, containingPath, IBase.class);
259                for (IBase nextElement : containingElements) {
260
261                        ChildDefinition childDefinition = findChildDefinition(nextElement, elementName);
262
263                        List<IBase> existingValues =
264                                        new ArrayList<>(childDefinition.getChildDef().getAccessor().getValues(nextElement));
265                        if (removeIndex == null || removeIndex < 0 || removeIndex >= existingValues.size()) {
266                                String msg = myContext
267                                                .getLocalizer()
268                                                .getMessage(
269                                                                FhirPatch.class, "invalidMoveSourceIndex", removeIndex, path, existingValues.size());
270                                throw new InvalidRequestException(Msg.code(1268) + msg);
271                        }
272                        IBase newValue = existingValues.remove(removeIndex.intValue());
273
274                        if (insertIndex == null || insertIndex < 0 || insertIndex > existingValues.size()) {
275                                String msg = myContext
276                                                .getLocalizer()
277                                                .getMessage(
278                                                                FhirPatch.class,
279                                                                "invalidMoveDestinationIndex",
280                                                                insertIndex,
281                                                                path,
282                                                                existingValues.size());
283                                throw new InvalidRequestException(Msg.code(1269) + msg);
284                        }
285                        existingValues.add(insertIndex, newValue);
286
287                        childDefinition.getChildDef().getMutator().setValue(nextElement, null);
288                        for (IBase nextNewValue : existingValues) {
289                                childDefinition.getChildDef().getMutator().addValue(nextElement, nextNewValue);
290                        }
291                }
292        }
293
294        private ChildDefinition findChildDefinition(IBase theContainingElement, String theElementName) {
295                BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(theContainingElement.getClass());
296
297                String childName = theElementName;
298                BaseRuntimeChildDefinition childDef = elementDef.getChildByName(childName);
299                BaseRuntimeElementDefinition<?> childElement;
300                if (childDef == null) {
301                        childName = theElementName + "[x]";
302                        childDef = elementDef.getChildByName(childName);
303                        childElement = childDef.getChildByName(
304                                        childDef.getValidChildNames().iterator().next());
305                } else {
306                        childElement = childDef.getChildByName(childName);
307                }
308
309                return new ChildDefinition(childDef, childElement);
310        }
311
312        private IBase getNewValue(IBase theParameters, IBase theElement, ChildDefinition theChildDefinition) {
313                Optional<IBase> valuePart = ParametersUtil.getParameterPart(myContext, theParameters, PARAMETER_VALUE);
314                Optional<IBase> valuePartValue =
315                                ParametersUtil.getParameterPartValue(myContext, theParameters, PARAMETER_VALUE);
316
317                IBase newValue;
318                if (valuePartValue.isPresent()) {
319                        newValue = valuePartValue.get();
320                } else {
321                        List<IBase> partParts = valuePart.map(this::extractPartsFromPart).orElse(Collections.emptyList());
322
323                        newValue = createAndPopulateNewElement(theChildDefinition, partParts);
324                }
325
326                if (IBaseEnumeration.class.isAssignableFrom(
327                                                theChildDefinition.getChildElement().getImplementingClass())
328                                || XhtmlNode.class.isAssignableFrom(
329                                                theChildDefinition.getChildElement().getImplementingClass())) {
330                        // If the compositeElementDef is an IBaseEnumeration, we will use the actual compositeElementDef definition
331                        // to build one, since
332                        // it needs the right factory object passed to its constructor
333                        IPrimitiveType<?> newValueInstance;
334                        if (theChildDefinition.getChildDef().getInstanceConstructorArguments() != null) {
335                                newValueInstance = (IPrimitiveType<?>) theChildDefinition
336                                                .getChildElement()
337                                                .newInstance(theChildDefinition.getChildDef().getInstanceConstructorArguments());
338                        } else {
339                                newValueInstance =
340                                                (IPrimitiveType<?>) theChildDefinition.getChildElement().newInstance();
341                        }
342                        newValueInstance.setValueAsString(((IPrimitiveType<?>) newValue).getValueAsString());
343                        theChildDefinition.getChildDef().getMutator().setValue(theElement, newValueInstance);
344                        newValue = newValueInstance;
345                }
346                return newValue;
347        }
348
349        @Nonnull
350        private List<IBase> extractPartsFromPart(IBase theParametersParameterComponent) {
351                return myContext.newTerser().getValues(theParametersParameterComponent, "part");
352        }
353
354        /**
355         * this method will instantiate an element according to the provided Definition and it according to
356         * the properties found in thePartParts.  a part usually represent a datatype as a name/value[X] pair.
357         * it may also represent a complex type like an Extension.
358         *
359         * @param theDefinition wrapper around the runtime definition of the element to be populated
360         * @param thePartParts list of Part to populate the element that will be created from theDefinition
361         * @return an element that was created from theDefinition and populated with the parts
362         */
363        private IBase createAndPopulateNewElement(ChildDefinition theDefinition, List<IBase> thePartParts) {
364                IBase newElement = theDefinition.getChildElement().newInstance();
365
366                for (IBase nextValuePartPart : thePartParts) {
367
368                        String name = myContext
369                                        .newTerser()
370                                        .getSingleValue(nextValuePartPart, PARAMETER_NAME, IPrimitiveType.class)
371                                        .map(IPrimitiveType::getValueAsString)
372                                        .orElse(null);
373
374                        if (StringUtils.isBlank(name)) {
375                                continue;
376                        }
377
378                        Optional<IBase> value = myContext.newTerser().getSingleValue(nextValuePartPart, "value[x]", IBase.class);
379
380                        if (value.isPresent()) {
381                                // we have a dataType. let's extract its value and assign it.
382                                BaseRuntimeChildDefinition partChildDef =
383                                                theDefinition.getChildElement().getChildByName(name);
384                                if (partChildDef == null) {
385                                        name = name + "[x]";
386                                        partChildDef = theDefinition.getChildElement().getChildByName(name);
387                                }
388                                partChildDef.getMutator().addValue(newElement, value.get());
389
390                                // a part represent a datatype or a complexType but not both at the same time.
391                                continue;
392                        }
393
394                        List<IBase> part = extractPartsFromPart(nextValuePartPart);
395
396                        if (!part.isEmpty()) {
397                                // we have a complexType.  let's find its definition and recursively process
398                                // them till all complexTypes are processed.
399                                ChildDefinition childDefinition = findChildDefinition(newElement, name);
400
401                                IBase childNewValue = createAndPopulateNewElement(childDefinition, part);
402
403                                childDefinition.getChildDef().getMutator().setValue(newElement, childNewValue);
404                        }
405                }
406
407                return newElement;
408        }
409
410        private void deleteSingleElement(IBase theElementToDelete) {
411                myContext.newTerser().visit(theElementToDelete, new IModelVisitor2() {
412                        @Override
413                        public boolean acceptElement(
414                                        IBase theElement,
415                                        List<IBase> theContainingElementPath,
416                                        List<BaseRuntimeChildDefinition> theChildDefinitionPath,
417                                        List<BaseRuntimeElementDefinition<?>> theElementDefinitionPath) {
418                                if (theElement instanceof IPrimitiveType) {
419                                        ((IPrimitiveType<?>) theElement).setValueAsString(null);
420                                }
421                                return true;
422                        }
423                });
424        }
425
426        public IBaseParameters diff(@Nullable IBaseResource theOldValue, @Nonnull IBaseResource theNewValue) {
427                IBaseParameters retVal = ParametersUtil.newInstance(myContext);
428                String newValueTypeName = myContext.getResourceDefinition(theNewValue).getName();
429
430                if (theOldValue == null) {
431
432                        IBase operation = ParametersUtil.addParameterToParameters(myContext, retVal, PARAMETER_OPERATION);
433                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
434                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, newValueTypeName);
435                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, theNewValue);
436
437                } else {
438
439                        String oldValueTypeName =
440                                        myContext.getResourceDefinition(theOldValue).getName();
441                        Validate.isTrue(oldValueTypeName.equalsIgnoreCase(newValueTypeName), "Resources must be of same type");
442
443                        BaseRuntimeElementCompositeDefinition<?> def =
444                                        myContext.getResourceDefinition(theOldValue).getBaseDefinition();
445                        String path = def.getName();
446
447                        EncodeContextPath contextPath = new EncodeContextPath();
448                        contextPath.pushPath(path, true);
449
450                        compare(retVal, contextPath, def, path, path, theOldValue, theNewValue);
451
452                        contextPath.popPath();
453                        assert contextPath.getPath().isEmpty();
454                }
455
456                return retVal;
457        }
458
459        private void compare(
460                        IBaseParameters theDiff,
461                        EncodeContextPath theSourceEncodeContext,
462                        BaseRuntimeElementDefinition<?> theDef,
463                        String theSourcePath,
464                        String theTargetPath,
465                        IBase theOldField,
466                        IBase theNewField) {
467
468                boolean pathIsIgnored = pathIsIgnored(theSourceEncodeContext);
469                if (pathIsIgnored) {
470                        return;
471                }
472
473                BaseRuntimeElementDefinition<?> sourceDef = myContext.getElementDefinition(theOldField.getClass());
474                BaseRuntimeElementDefinition<?> targetDef = myContext.getElementDefinition(theNewField.getClass());
475                if (!sourceDef.getName().equals(targetDef.getName())) {
476                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
477                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
478                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
479                        addValueToDiff(operation, theOldField, theNewField);
480                } else {
481                        if (theOldField instanceof IPrimitiveType) {
482                                IPrimitiveType<?> oldPrimitive = (IPrimitiveType<?>) theOldField;
483                                IPrimitiveType<?> newPrimitive = (IPrimitiveType<?>) theNewField;
484                                String oldValueAsString = toValue(oldPrimitive);
485                                String newValueAsString = toValue(newPrimitive);
486                                if (!Objects.equals(oldValueAsString, newValueAsString)) {
487                                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
488                                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_REPLACE);
489                                        ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, theTargetPath);
490                                        addValueToDiff(operation, oldPrimitive, newPrimitive);
491                                }
492                        }
493
494                        List<BaseRuntimeChildDefinition> children = theDef.getChildren();
495                        for (BaseRuntimeChildDefinition nextChild : children) {
496                                compareField(
497                                                theDiff,
498                                                theSourceEncodeContext,
499                                                theSourcePath,
500                                                theTargetPath,
501                                                theOldField,
502                                                theNewField,
503                                                nextChild);
504                        }
505                }
506        }
507
508        private void compareField(
509                        IBaseParameters theDiff,
510                        EncodeContextPath theSourceEncodePath,
511                        String theSourcePath,
512                        String theTargetPath,
513                        IBase theOldField,
514                        IBase theNewField,
515                        BaseRuntimeChildDefinition theChildDef) {
516                String elementName = theChildDef.getElementName();
517                boolean repeatable = theChildDef.getMax() != 1;
518                theSourceEncodePath.pushPath(elementName, false);
519                if (pathIsIgnored(theSourceEncodePath)) {
520                        theSourceEncodePath.popPath();
521                        return;
522                }
523
524                List<? extends IBase> sourceValues = theChildDef.getAccessor().getValues(theOldField);
525                List<? extends IBase> targetValues = theChildDef.getAccessor().getValues(theNewField);
526
527                int sourceIndex = 0;
528                int targetIndex = 0;
529                while (sourceIndex < sourceValues.size() && targetIndex < targetValues.size()) {
530
531                        IBase sourceChildField = sourceValues.get(sourceIndex);
532                        Validate.notNull(sourceChildField); // not expected to happen, but just in case
533                        BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(sourceChildField.getClass());
534                        IBase targetChildField = targetValues.get(targetIndex);
535                        Validate.notNull(targetChildField); // not expected to happen, but just in case
536                        String sourcePath = theSourcePath + "." + elementName + (repeatable ? "[" + sourceIndex + "]" : "");
537                        String targetPath = theSourcePath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : "");
538
539                        compare(theDiff, theSourceEncodePath, def, sourcePath, targetPath, sourceChildField, targetChildField);
540
541                        sourceIndex++;
542                        targetIndex++;
543                }
544
545                // Find newly inserted items
546                while (targetIndex < targetValues.size()) {
547                        String path = theTargetPath + "." + elementName;
548                        addInsertItems(theDiff, targetValues, targetIndex, path, theChildDef);
549                        targetIndex++;
550                }
551
552                // Find deleted items
553                while (sourceIndex < sourceValues.size()) {
554                        IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
555                        ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_DELETE);
556                        ParametersUtil.addPartString(
557                                        myContext,
558                                        operation,
559                                        PARAMETER_PATH,
560                                        theTargetPath + "." + elementName + (repeatable ? "[" + targetIndex + "]" : ""));
561
562                        sourceIndex++;
563                        targetIndex++;
564                }
565
566                theSourceEncodePath.popPath();
567        }
568
569        private void addInsertItems(
570                        IBaseParameters theDiff,
571                        List<? extends IBase> theTargetValues,
572                        int theTargetIndex,
573                        String thePath,
574                        BaseRuntimeChildDefinition theChildDefinition) {
575                IBase operation = ParametersUtil.addParameterToParameters(myContext, theDiff, PARAMETER_OPERATION);
576                ParametersUtil.addPartCode(myContext, operation, PARAMETER_TYPE, OPERATION_INSERT);
577                ParametersUtil.addPartString(myContext, operation, PARAMETER_PATH, thePath);
578                ParametersUtil.addPartInteger(myContext, operation, PARAMETER_INDEX, theTargetIndex);
579
580                IBase value = theTargetValues.get(theTargetIndex);
581                BaseRuntimeElementDefinition<?> valueDef = myContext.getElementDefinition(value.getClass());
582
583                /*
584                 * If the value is a Resource or a datatype, we can put it into the part.value and that will cover
585                 * all of its children. If it's an infrastructure element though, such as Patient.contact we can't
586                 * just put it into part.value because it isn't an actual type. So we have to put all of its
587                 * children in instead.
588                 */
589                if (valueDef.isStandardType()) {
590                        ParametersUtil.addPart(myContext, operation, PARAMETER_VALUE, value);
591                } else {
592                        for (BaseRuntimeChildDefinition nextChild : valueDef.getChildren()) {
593                                List<IBase> childValues = nextChild.getAccessor().getValues(value);
594                                for (int index = 0; index < childValues.size(); index++) {
595                                        boolean childRepeatable = theChildDefinition.getMax() != 1;
596                                        String elementName = nextChild.getChildNameByDatatype(
597                                                        childValues.get(index).getClass());
598                                        String targetPath = thePath + (childRepeatable ? "[" + index + "]" : "") + "." + elementName;
599                                        addInsertItems(theDiff, childValues, index, targetPath, nextChild);
600                                }
601                        }
602                }
603        }
604
605        private void addValueToDiff(IBase theOperationPart, IBase theOldValue, IBase theNewValue) {
606
607                if (myIncludePreviousValueInDiff) {
608                        IBase oldValue = massageValueForDiff(theOldValue);
609                        ParametersUtil.addPart(myContext, theOperationPart, "previousValue", oldValue);
610                }
611
612                IBase newValue = massageValueForDiff(theNewValue);
613                ParametersUtil.addPart(myContext, theOperationPart, PARAMETER_VALUE, newValue);
614        }
615
616        private boolean pathIsIgnored(EncodeContextPath theSourceEncodeContext) {
617                boolean pathIsIgnored = false;
618                for (EncodeContextPath next : myIgnorePaths) {
619                        if (theSourceEncodeContext.startsWith(next, false)) {
620                                pathIsIgnored = true;
621                                break;
622                        }
623                }
624                return pathIsIgnored;
625        }
626
627        private IBase massageValueForDiff(IBase theNewValue) {
628                IBase massagedValue = theNewValue;
629
630                // XHTML content is dealt with by putting it in a string
631                if (theNewValue instanceof XhtmlNode) {
632                        String xhtmlString = ((XhtmlNode) theNewValue).getValueAsString();
633                        massagedValue = myContext.getElementDefinition("string").newInstance(xhtmlString);
634                }
635
636                // IIdType can hold a fully qualified ID, but we just want the ID part to show up in diffs
637                if (theNewValue instanceof IIdType) {
638                        String idPart = ((IIdType) theNewValue).getIdPart();
639                        massagedValue = myContext.getElementDefinition("id").newInstance(idPart);
640                }
641
642                return massagedValue;
643        }
644
645        private String toValue(IPrimitiveType<?> theOldPrimitive) {
646                if (theOldPrimitive instanceof IIdType) {
647                        return ((IIdType) theOldPrimitive).getIdPart();
648                }
649                return theOldPrimitive.getValueAsString();
650        }
651
652        private static class ChildDefinition {
653                private final BaseRuntimeChildDefinition myChildDef;
654                private final BaseRuntimeElementDefinition<?> myChildElement;
655
656                public ChildDefinition(
657                                BaseRuntimeChildDefinition theChildDef, BaseRuntimeElementDefinition<?> theChildElement) {
658                        this.myChildDef = theChildDef;
659                        this.myChildElement = theChildElement;
660                }
661
662                public BaseRuntimeChildDefinition getChildDef() {
663                        return myChildDef;
664                }
665
666                public BaseRuntimeElementDefinition<?> getChildElement() {
667                        return myChildElement;
668                }
669        }
670}