001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Antoine Taillefer
018 */
019package org.nuxeo.ecm.diff.service.impl;
020
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.List;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.custommonkey.xmlunit.Difference;
028import org.custommonkey.xmlunit.DifferenceConstants;
029import org.custommonkey.xmlunit.NodeDetail;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.io.ExportConstants;
032import org.nuxeo.ecm.diff.model.DifferenceType;
033import org.nuxeo.ecm.diff.model.DocumentDiff;
034import org.nuxeo.ecm.diff.model.PropertyDiff;
035import org.nuxeo.ecm.diff.model.PropertyType;
036import org.nuxeo.ecm.diff.model.SchemaDiff;
037import org.nuxeo.ecm.diff.model.impl.ComplexPropertyDiff;
038import org.nuxeo.ecm.diff.model.impl.ContentProperty;
039import org.nuxeo.ecm.diff.model.impl.ContentPropertyDiff;
040import org.nuxeo.ecm.diff.model.impl.ListPropertyDiff;
041import org.nuxeo.ecm.diff.model.impl.PropertyHierarchyNode;
042import org.nuxeo.ecm.diff.model.impl.SimplePropertyDiff;
043import org.w3c.dom.NamedNodeMap;
044import org.w3c.dom.Node;
045import org.w3c.dom.NodeList;
046
047/**
048 * Helper for computing a field diff.
049 *
050 * @author <a href="mailto:ataillefer@nuxeo.com">Antoine Taillefer</a>
051 * @since 5.6
052 */
053public final class FieldDiffHelper {
054
055    private static final Log LOGGER = LogFactory.getLog(FieldDiffHelper.class);
056
057    public static final String FACET_ELEMENT = "facet";
058
059    public static final String SCHEMA_ELEMENT = "schema";
060
061    public static final String NAME_ATTRIBUTE = "name";
062
063    public static final String TYPE_ATTRIBUTE = "type";
064
065    /**
066     * Computes a field diff.
067     * <p>
068     * First gets all needed elements to compute the field diff:
069     * <ul>
070     * <li>propertyHierarchy: list holding the property hierarchy
071     *
072     * <pre>
073     * Every time we encounter a list, a complex or a content node going up in the DOM tree
074     * from the property node to the prefixed field node, we add it to the property
075     * hierarchy.
076     * If it is a list item node, we set its index in the hierarchy.
077     * If it is a complex item node, we set its name in the hierarchy.
078     * If it is a content item node (ie. "encoding", "mime-type", "filename" or "digest"),
079     * we set its name in the hierarchy.
080     *
081     * Example: complex list
082     *
083     * The "true" property's hierarchy is:
084     * [{list,"0"},{complex, "complexBoolean"}]
085     *
086     * The "jack" property's hierarchy is:
087     * [{list,"1"},{complex, "complexString"}]
088     *
089     * The "UTF-8" property's hierarchy is:
090     * [{list,"0"},{complex, "complexString"},{content, "encoding"}]
091     *
092     * <list type="complexList">
093     *   <complexItem type="complex">
094     *     <complexString type="string">joe</complexString>
095     *     <complexBoolean type="boolean">true</complexBoolean>
096     *     <complexContent type="content">
097     *      <encoding>UTF-8</encoding>
098     *      <mime-type>text/plain</mime-type>
099     *      <filename>My_file.txt</filename>
100     *      <digest>5dafdabf966043c8c8cef20011e939a2</digest>
101     *     </complexContent>
102     *   </complexItem>
103     *   <complexItem type="complex">
104     *     <complexString type="string">jack</complexString>
105     *     <complexBoolean type="boolean">false</complexBoolean>
106     *   </complexItem>
107     * </list>
108     * </pre>
109     *
110     * </li>
111     * </ul>
112     *
113     * @param docDiff the doc diff
114     * @param controlNodeDetail the control node detail
115     * @param testNodeDetail the test node detail
116     * @param fieldDifferenceCount the field difference countadd
117     * @param difference the difference
118     * @return true if a field diff has been found
119     */
120    public static boolean computeFieldDiff(DocumentDiff docDiff, NodeDetail controlNodeDetail,
121            NodeDetail testNodeDetail, int fieldDifferenceCount, Difference difference) {
122
123        // Use control node or if null test node to detect schema and
124        // field elements
125        Node currentNode = controlNodeDetail.getNode();
126        if (currentNode == null) {
127            currentNode = testNodeDetail.getNode();
128        }
129        if (currentNode != null) {
130
131            String field = null;
132            String currentNodeName = currentNode.getNodeName();
133            List<PropertyHierarchyNode> propertyHierarchy = new ArrayList<PropertyHierarchyNode>();
134
135            // Detect a schema element,
136            // for instance: <schema name="dublincore" xmlns:dc="...">.
137            // Otherwise build the property hierarchy.
138            // For a content type property (blob) don't take into account a
139            // difference on the "data" field, since what makes the difference
140            // between 2 blobs is either the filename or the digest.
141            Node parentNode = currentNode.getParentNode();
142            while (parentNode != null && !SCHEMA_ELEMENT.equals(currentNodeName)
143                    && !ExportConstants.BLOB_DATA.equals(parentNode.getNodeName())) {
144
145                // Get property type
146                String propertyType = getPropertyType(currentNode);
147                String parentPropertyType = getPropertyType(parentNode);
148
149                // Fill in property hierarchy
150                if (PropertyType.isListType(parentPropertyType)) {
151                    int currentNodePosition = getNodePosition(currentNode);
152                    propertyHierarchy.add(new PropertyHierarchyNode(parentPropertyType,
153                            String.valueOf(currentNodePosition)));
154                } else if (PropertyType.isComplexType(parentPropertyType)
155                        || PropertyType.isContentType(parentPropertyType)) {
156                    propertyHierarchy.add(new PropertyHierarchyNode(parentPropertyType, currentNodeName));
157                }
158
159                // Detect a field element, ie. an element that has a
160                // prefix, for instance: <dc:title>.
161                if (SCHEMA_ELEMENT.equals(parentNode.getNodeName())) {
162                    String currentNodeLocalName = currentNode.getLocalName();
163                    // TODO: manage better the facet case
164                    if (!FACET_ELEMENT.equals(currentNodeLocalName)) {
165                        field = currentNodeLocalName;
166                        if (PropertyType.isSimpleType(propertyType) || PropertyType.isListType(propertyType)
167                                && propertyHierarchy.isEmpty() || PropertyType.isComplexType(propertyType)
168                                && propertyHierarchy.isEmpty() || PropertyType.isContentType(propertyType)
169                                && propertyHierarchy.isEmpty()) {
170                            propertyHierarchy.add(new PropertyHierarchyNode(propertyType, null));
171                        }
172                    }
173                }
174                currentNode = parentNode;
175                currentNodeName = currentNode.getNodeName();
176                parentNode = parentNode.getParentNode();
177            }
178
179            // If we found a schema element (ie. we did not
180            // reached the root element, ie. parentNode != null) and a
181            // nested field element, we can compute the diff for this
182            // field.
183            if (parentNode != null && field != null && !propertyHierarchy.isEmpty()) {
184                String schema = currentNodeName;
185                // Get schema name
186                NamedNodeMap attr = currentNode.getAttributes();
187                if (attr != null && attr.getLength() > 0) {
188                    Node nameAttr = attr.getNamedItem(NAME_ATTRIBUTE);
189                    if (nameAttr != null) {
190                        schema = nameAttr.getNodeValue();
191                    }
192                }
193
194                // Reverse property hierarchy
195                Collections.reverse(propertyHierarchy);
196
197                // Pretty log field difference
198                LOGGER.debug(String.format(
199                        "Found field difference #%d on [%s]/[%s] with hierarchy %s: [%s (%s)] {%s --> %s}",
200                        fieldDifferenceCount + 1, schema, field, propertyHierarchy, difference.getDescription(),
201                        difference.getId(), controlNodeDetail.getValue(), testNodeDetail.getValue()));
202
203                // Compute field diff
204                computeFieldDiff(docDiff, schema, field, propertyHierarchy, difference.getId(), controlNodeDetail,
205                        testNodeDetail);
206                // Return true since a field diff has been found
207                return true;
208
209            } else {// Non-field difference
210                LOGGER.debug(String.format("Found non-field difference: [%s (%s)] {%s --> %s}",
211                        difference.getDescription(), difference.getId(), controlNodeDetail.getValue(),
212                        testNodeDetail.getValue()));
213            }
214        }
215        return false;
216    }
217
218    /**
219     * Gets the node property type.
220     *
221     * @param node the node
222     * @return the property diff type
223     */
224    public static String getPropertyType(Node node) {
225
226        // Default: undefined
227        String propertyType = PropertyType.UNDEFINED;
228
229        NamedNodeMap nodeAttr = node.getAttributes();
230        if (nodeAttr != null) {
231            Node type = nodeAttr.getNamedItem(TYPE_ATTRIBUTE);
232            if (type != null) {
233                propertyType = type.getNodeValue();
234            }
235        }
236
237        return propertyType;
238    }
239
240    /**
241     * Gets the node position.
242     *
243     * @param node the node
244     * @return the node position
245     */
246    private static int getNodePosition(Node node) {
247
248        int nodePos = 0;
249        Node previousSibling = node.getPreviousSibling();
250        while (previousSibling != null) {
251            nodePos++;
252            previousSibling = previousSibling.getPreviousSibling();
253        }
254        return nodePos;
255    }
256
257    /**
258     * Sets the property diff hierarchy.
259     *
260     * @param firstPropertyDiff the first property diff
261     * @param propertyHierarchy the property hierarchy
262     * @return the property diff
263     */
264    public static PropertyDiff applyPropertyHierarchyToDiff(PropertyDiff firstPropertyDiff,
265            List<PropertyHierarchyNode> propertyHierarchy) {
266
267        if (propertyHierarchy.isEmpty()) {
268            throw new NuxeoException("Empty property hierarchy.");
269        }
270
271        // Get first property hierarchy node
272        PropertyHierarchyNode propertyHierarchyNode = propertyHierarchy.get(0);
273        String firstPropertyType = propertyHierarchyNode.getNodeType();
274        String firstPropertyValue = propertyHierarchyNode.getNodeValue();
275
276        if ((PropertyType.isSimpleType(firstPropertyType) || PropertyType.isContentType(firstPropertyType))
277                && propertyHierarchy.size() > 1) {
278            throw new NuxeoException(String.format("Inconsistant property hierarchy %s.", propertyHierarchy));
279        }
280
281        // Go through the property hierarchy
282        PropertyDiff propertyDiff = firstPropertyDiff;
283        String propertyType = firstPropertyType;
284        String propertyValue = firstPropertyValue;
285        for (int i = 1; i < propertyHierarchy.size(); i++) {
286
287            PropertyDiff childPropertyDiff = null;
288            PropertyHierarchyNode childPropertyHierarchyNode = propertyHierarchy.get(i);
289            String childPropertyType = childPropertyHierarchyNode.getNodeType();
290            String childPropertyValue = childPropertyHierarchyNode.getNodeValue();
291
292            // Simple or content type
293            if (PropertyType.isSimpleType(propertyType) || PropertyType.isContentType(propertyType)) {
294                // Nothing to do here (should never happen)
295            }
296            // List type
297            else if (PropertyType.isListType(propertyType)) {
298                int propertyIndex = Integer.parseInt(propertyValue);
299                // Get list diff, if null create a new one
300                childPropertyDiff = ((ListPropertyDiff) propertyDiff).getDiff(propertyIndex);
301                if (childPropertyDiff == null) {
302                    childPropertyDiff = newPropertyDiff(childPropertyType);
303                    ((ListPropertyDiff) propertyDiff).putDiff(propertyIndex, childPropertyDiff);
304                }
305                propertyDiff = childPropertyDiff;
306            }
307            // Complex type
308            else {
309                // Get complex diff, initialize it if null
310                childPropertyDiff = ((ComplexPropertyDiff) propertyDiff).getDiff(propertyValue);
311                if (childPropertyDiff == null) {
312                    childPropertyDiff = newPropertyDiff(childPropertyType);
313                    ((ComplexPropertyDiff) propertyDiff).putDiff(propertyValue, childPropertyDiff);
314                }
315                propertyDiff = childPropertyDiff;
316            }
317
318            propertyType = childPropertyType;
319            propertyValue = childPropertyValue;
320        }
321        return propertyDiff;
322    }
323
324    /**
325     * Computes a field diff.
326     *
327     * @param docDiff the doc diff
328     * @param schema the schema
329     * @param field the field
330     * @param propertyHierarchy the property hierarchy
331     * @param differenceId the difference id
332     * @param controlNodeDetail the control node detail
333     * @param testNodeDetail the test node detail
334     */
335    private static void computeFieldDiff(DocumentDiff docDiff, String schema, String field,
336            List<PropertyHierarchyNode> propertyHierarchy, int differenceId, NodeDetail controlNodeDetail,
337            NodeDetail testNodeDetail) {
338
339        if (propertyHierarchy.isEmpty()) {
340            throw new NuxeoException("Empty property hierarchy.");
341        }
342
343        // Get first property hierarchy node
344        PropertyHierarchyNode propertyHierarchyNode = propertyHierarchy.get(0);
345        String firstPropertyType = propertyHierarchyNode.getNodeType();
346
347        // Get schema diff, initialize it if null
348        SchemaDiff schemaDiff = docDiff.getSchemaDiff(schema);
349        if (schemaDiff == null) {
350            schemaDiff = docDiff.initSchemaDiff(schema);
351        }
352
353        // Get field diff, initialize it if null
354        PropertyDiff fieldDiff = schemaDiff.getFieldDiff(field);
355        if (fieldDiff == null) {
356            fieldDiff = newPropertyDiff(firstPropertyType);
357        }
358
359        PropertyDiff endPropertyDiff = fieldDiff;
360        // Apply property hierarchy to diff if first property type in hierarchy
361        // is list or complex
362        if (!(PropertyType.isSimpleType(firstPropertyType))) {
363            endPropertyDiff = applyPropertyHierarchyToDiff(fieldDiff, propertyHierarchy);
364        }
365
366        // Compute field diff depending on difference type.
367        switch (differenceId) {
368        case DifferenceConstants.TEXT_VALUE_ID:
369            computeTextValueDiff(endPropertyDiff, controlNodeDetail, testNodeDetail);
370            break;
371        case DifferenceConstants.CHILD_NODE_NOT_FOUND_ID:
372            computeChildNodeNotFoundDiff(endPropertyDiff, controlNodeDetail, testNodeDetail);
373            break;
374        case DifferenceConstants.HAS_CHILD_NODES_ID:
375            computeHasChildNodesDiff(endPropertyDiff, controlNodeDetail, testNodeDetail);
376            break;
377        default:
378            computeTextValueDiff(endPropertyDiff, controlNodeDetail, testNodeDetail);
379        }
380
381        schemaDiff.putFieldDiff(field, fieldDiff);
382    }
383
384    /**
385     * New property diff.
386     *
387     * @param propertyType the property type
388     * @return the property diff
389     */
390    private static PropertyDiff newPropertyDiff(String propertyType) {
391
392        if (PropertyType.isSimpleType(propertyType)) {
393            return new SimplePropertyDiff(propertyType);
394        } else if (PropertyType.isListType(propertyType)) {
395            return new ListPropertyDiff(propertyType);
396        } else if (PropertyType.isComplexType(propertyType)) {
397            return new ComplexPropertyDiff();
398        } else { // Content type
399            return new ContentPropertyDiff();
400        }
401    }
402
403    /**
404     * Computes a TEXT_VALUE diff.
405     *
406     * @param fieldDiff the field diff
407     * @param controlNodeDetail the control node detail
408     * @param testNodeDetail the test node detail
409     */
410    private static void computeTextValueDiff(PropertyDiff fieldDiff, NodeDetail controlNodeDetail,
411            NodeDetail testNodeDetail) {
412
413        String leftValue = controlNodeDetail.getValue();
414        String rightValue = testNodeDetail.getValue();
415
416        Node controlNode = controlNodeDetail.getNode();
417        if (controlNode == null) {
418            throw new NuxeoException("Control node should never be null.");
419        }
420
421        Node controlParentNode = controlNode.getParentNode();
422        if (controlParentNode == null) {
423            throw new NuxeoException("Control parent node should never be null.");
424        }
425
426        String controlParentNodePropertyType = getPropertyType(controlParentNode);
427        String fieldDiffPropertyType = fieldDiff.getPropertyType();
428        // Simple type
429        if (PropertyType.isSimpleType(fieldDiffPropertyType)) {
430            ((SimplePropertyDiff) fieldDiff).setLeftValue(leftValue);
431            ((SimplePropertyDiff) fieldDiff).setRightValue(rightValue);
432        }
433        // List type
434        else if (PropertyType.isListType(fieldDiffPropertyType)) {
435            ((ListPropertyDiff) fieldDiff).putDiff(getNodePosition(controlParentNode), new SimplePropertyDiff(
436                    controlParentNodePropertyType, leftValue, rightValue));
437        }
438        // Complex type
439        else if (PropertyType.isComplexType(fieldDiffPropertyType)) {
440            ((ComplexPropertyDiff) fieldDiff).putDiff(controlParentNode.getNodeName(), new SimplePropertyDiff(
441                    controlParentNodePropertyType, leftValue, rightValue));
442        }
443        // Content type
444        else {
445            ContentPropertyDiff contentPropertyDiff = ((ContentPropertyDiff) fieldDiff);
446            setContentSubPropertyDiff(contentPropertyDiff, controlParentNode.getNodeName(), leftValue, rightValue);
447        }
448    }
449
450    /**
451     * Computes a CHILD_NODE_NOT_FOUND diff.
452     *
453     * @param fieldDiff the field diff
454     * @param controlNodeDetail the control node detail
455     * @param testNodeDetail the test node detail
456     */
457    private static void computeChildNodeNotFoundDiff(PropertyDiff fieldDiff, NodeDetail controlNodeDetail,
458            NodeDetail testNodeDetail) {
459
460        Node childNode;
461        boolean isTestNodeNotFound = "null".equals(testNodeDetail.getValue());
462        if (!isTestNodeNotFound) {
463            childNode = testNodeDetail.getNode();
464        } else {
465            childNode = controlNodeDetail.getNode();
466        }
467
468        if (childNode == null) {
469            throw new NuxeoException("Child node should never be null.");
470        }
471
472        String propertyType = fieldDiff.getPropertyType();
473        // Simple type
474        if (PropertyType.isSimpleType(propertyType)) {
475            // Should never happen as then it would be marked as a
476            // HAS_CHILD_NODES difference.
477            throw new NuxeoException("A CHILD_NODE_NOT_FOUND difference should never be found within a simple type.");
478        }
479        // List type
480        else if (PropertyType.isListType(propertyType)) {
481            PropertyDiff childNodeDiff = getChildNodePropertyDiff(childNode, isTestNodeNotFound);
482            ((ListPropertyDiff) fieldDiff).putDiff(getNodePosition(childNode), childNodeDiff);
483        }
484        // Complex type
485        else if (PropertyType.isComplexType(propertyType)) { // Complex type
486            throw new NuxeoException("A CHILD_NODE_NOT_FOUND difference should never be found within a complex type.");
487        }
488        // Content type
489        else {
490            throw new NuxeoException("A CHILD_NODE_NOT_FOUND difference should never be found within a content type.");
491        }
492    }
493
494    /**
495     * Computes a HAS_CHILD_NODES diff.
496     *
497     * @param fieldDiff the field diff
498     * @param controlNodeDetail the control node detail
499     * @param testNodeDetail the test node detail
500     */
501    private static void computeHasChildNodesDiff(PropertyDiff fieldDiff, NodeDetail controlNodeDetail,
502            NodeDetail testNodeDetail) {
503
504        Node nodeWithChildren;
505        boolean hasControlNodeChildNodes = Boolean.valueOf(controlNodeDetail.getValue());
506        if (hasControlNodeChildNodes) {
507            nodeWithChildren = controlNodeDetail.getNode();
508        } else {
509            nodeWithChildren = testNodeDetail.getNode();
510        }
511
512        if (nodeWithChildren == null) {
513            throw new NuxeoException("Node with children should never be null.");
514        }
515
516        String propertyType = fieldDiff.getPropertyType();
517        // Simple type
518        if (PropertyType.isSimpleType(propertyType)) {
519            setSimplePropertyDiff((SimplePropertyDiff) fieldDiff, nodeWithChildren, hasControlNodeChildNodes);
520        }
521        // List type
522        else if (PropertyType.isListType(propertyType)) {
523            PropertyDiff childNodeDiff = getChildNodePropertyDiff(nodeWithChildren, hasControlNodeChildNodes);
524            if (PropertyType.isListType(getPropertyType(nodeWithChildren))) {
525                ((ListPropertyDiff) fieldDiff).putAllDiff((ListPropertyDiff) childNodeDiff);
526            } else {
527                ((ListPropertyDiff) fieldDiff).putDiff(getNodePosition(nodeWithChildren), childNodeDiff);
528            }
529        }
530        // Complex type
531        else if (PropertyType.isComplexType(propertyType)) {
532            PropertyDiff childNodeDiff = getChildNodePropertyDiff(nodeWithChildren, hasControlNodeChildNodes);
533            if (PropertyType.isComplexType(getPropertyType(nodeWithChildren))) {
534                ((ComplexPropertyDiff) fieldDiff).putAllDiff((ComplexPropertyDiff) childNodeDiff);
535            } else {
536                ((ComplexPropertyDiff) fieldDiff).putDiff(nodeWithChildren.getNodeName(), childNodeDiff);
537            }
538        }
539        // Content type
540        else {
541            if (PropertyType.isContentType(getPropertyType(nodeWithChildren))) {
542                PropertyDiff childNodeDiff = getChildNodePropertyDiff(nodeWithChildren, hasControlNodeChildNodes);
543                ((ContentPropertyDiff) fieldDiff).setLeftContent(((ContentPropertyDiff) childNodeDiff).getLeftContent());
544                ((ContentPropertyDiff) fieldDiff).setRightContent(((ContentPropertyDiff) childNodeDiff).getRightContent());
545            } else {
546                setContentPropertyDiff((ContentPropertyDiff) fieldDiff, nodeWithChildren, hasControlNodeChildNodes);
547            }
548        }
549    }
550
551    /**
552     * Gets the child node property diff.
553     *
554     * @param node the node
555     * @param hasControlNodeChildNodes the test node was not found
556     */
557    private static PropertyDiff getChildNodePropertyDiff(Node node, boolean hasControlNodeChildNodes)
558            {
559
560        PropertyDiff propertyDiff;
561
562        String nodePropertyType = getPropertyType(node);
563
564        // Simple type
565        if (PropertyType.isSimpleType(nodePropertyType)) {
566            propertyDiff = new SimplePropertyDiff(nodePropertyType);
567            setSimplePropertyDiff((SimplePropertyDiff) propertyDiff, node, hasControlNodeChildNodes);
568        }
569        // List type
570        else if (PropertyType.isListType(nodePropertyType)) {
571            propertyDiff = new ListPropertyDiff(nodePropertyType);
572            NodeList childNodes = node.getChildNodes();
573            for (int i = 0; i < childNodes.getLength(); i++) {
574                ((ListPropertyDiff) propertyDiff).putDiff(i,
575                        getChildNodePropertyDiff(childNodes.item(i), hasControlNodeChildNodes));
576            }
577        }
578        // Complex type
579        else if (PropertyType.isComplexType(nodePropertyType)) {
580            propertyDiff = new ComplexPropertyDiff();
581            NodeList childNodes = node.getChildNodes();
582            for (int i = 0; i < childNodes.getLength(); i++) {
583                Node childNode = childNodes.item(i);
584                ((ComplexPropertyDiff) propertyDiff).putDiff(childNode.getNodeName(),
585                        getChildNodePropertyDiff(childNode, hasControlNodeChildNodes));
586            }
587        }
588        // Content type
589        else {
590            propertyDiff = new ContentPropertyDiff();
591            NodeList childNodes = node.getChildNodes();
592            for (int i = 0; i < childNodes.getLength(); i++) {
593                Node childNode = childNodes.item(i);
594                setContentPropertyDiff((ContentPropertyDiff) propertyDiff, childNode, hasControlNodeChildNodes);
595            }
596        }
597        return propertyDiff;
598    }
599
600    /**
601     * Sets the text content of textNode on {@link SimplePropertyDiff} field diff.
602     *
603     * @param fieldDiff the field diff
604     * @param textNode the text node
605     * @param hasControlNodeContent the has control node content
606     */
607    private static void setSimplePropertyDiff(SimplePropertyDiff fieldDiff, Node textNode, boolean hasControlNodeContent) {
608
609        String textNodeValue = textNode.getTextContent();
610
611        String leftValue = hasControlNodeContent ? textNodeValue : null;
612        String rightValue = hasControlNodeContent ? null : textNodeValue;
613
614        fieldDiff.setLeftValue(leftValue);
615        fieldDiff.setRightValue(rightValue);
616    }
617
618    /**
619     * Sets the text content of textNode on a {@link ContentPropertyDiff} field diff.
620     *
621     * @param fieldDiff the field diff
622     * @param textNode the text node
623     * @param hasControlNodeContent the has control node content
624     */
625    private static void setContentPropertyDiff(ContentPropertyDiff fieldDiff, Node textNode,
626            boolean hasControlNodeContent) {
627
628        String textNodeValue = textNode.getTextContent();
629
630        String leftValue = hasControlNodeContent ? textNodeValue : null;
631        String rightValue = hasControlNodeContent ? null : textNodeValue;
632
633        setContentSubPropertyDiff(fieldDiff, textNode.getNodeName(), leftValue, rightValue);
634    }
635
636    protected static void setContentSubPropertyDiff(ContentPropertyDiff fieldDiff, String subPropertyName,
637            String leftSubPropertyValue, String rightSubPropertyValue) {
638
639        // Get or initialize left and right content
640        ContentProperty leftContent = fieldDiff.getLeftContent();
641        ContentProperty rightContent = fieldDiff.getRightContent();
642        if (leftContent == null) {
643            leftContent = new ContentProperty();
644            fieldDiff.setLeftContent(leftContent);
645        }
646        if (rightContent == null) {
647            rightContent = new ContentProperty();
648            fieldDiff.setRightContent(rightContent);
649        }
650
651        // Set sub property on left and right content
652        leftContent.setSubProperty(subPropertyName, leftSubPropertyValue);
653        rightContent.setSubProperty(subPropertyName, rightSubPropertyValue);
654
655        // Set difference type on content property diff
656        if (ExportConstants.BLOB_FILENAME.equals(subPropertyName)) {
657            if (DifferenceType.differentDigest.equals(fieldDiff.getDifferenceType())) {
658                fieldDiff.setDifferenceType(DifferenceType.different);
659            } else {
660                fieldDiff.setDifferenceType(DifferenceType.differentFilename);
661            }
662        } else if (ExportConstants.BLOB_DIGEST.equals(subPropertyName)) {
663            if (DifferenceType.differentFilename.equals(fieldDiff.getDifferenceType())) {
664                fieldDiff.setDifferenceType(DifferenceType.different);
665            } else {
666                fieldDiff.setDifferenceType(DifferenceType.differentDigest);
667            }
668        }
669    }
670}