org.jahia.services.content.ConflictResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.content.ConflictResolver.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.content;

import org.apache.commons.collections.map.ListOrderedMap;
import org.apache.commons.lang.StringUtils;
import org.jahia.api.Constants;
import org.slf4j.Logger;

import javax.jcr.*;

import java.util.*;

/**
 * User: toto
 * Date: Feb 1, 2010
 * Time: 5:58:37 PM
 */
public class ConflictResolver {

    private static final List<String> IGNORED_PROPRTIES = Arrays.asList(Constants.JCR_UUID,
            Constants.JCR_PRIMARYTYPE, Constants.JCR_CREATED, Constants.JCR_CREATEDBY, Constants.JCR_BASEVERSION,
            Constants.JCR_ISCHECKEDOUT, Constants.JCR_VERSIONHISTORY, Constants.JCR_PREDECESSORS,
            Constants.JCR_ACTIVITY, Constants.CHECKIN_DATE, "j:locktoken", "j:lockTypes", "jcr:lockOwner",
            "jcr:lockIsDeep", "j:deletedChildren", "j:processId", "lastReplay");

    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(ConflictResolver.class);

    private JCRNodeWrapper sourceNode;
    private JCRNodeWrapper targetNode;
    private Set<JCRNodeWrapper> toCheckpoint;
    private List<Diff> differences;
    private List<Diff> unresolvedDifferences;

    public ConflictResolver(JCRNodeWrapper sourceNode, JCRNodeWrapper targetNode) throws RepositoryException {
        this.sourceNode = sourceNode;
        this.targetNode = targetNode;
    }

    public void setToCheckpoint(Set<JCRNodeWrapper> toCheckpoint) {
        this.toCheckpoint = toCheckpoint;
    }

    public List<Diff> getUnresolvedDifferences() {
        return unresolvedDifferences;
    }

    public void applyDifferences() throws RepositoryException {
        computeDifferences();
        unresolvedDifferences = new ArrayList<Diff>();
        for (Diff diff : differences) {
            if (!diff.apply()) {
                unresolvedDifferences.add(diff);
            }
        }
        targetNode.getSession().save();
    }

    private void computeDifferences() throws RepositoryException {
        differences = compare(sourceNode, targetNode, "");
    }

    private List<Diff> compare(JCRNodeWrapper sourceNode, JCRNodeWrapper targetNode, String basePath)
            throws RepositoryException {

        List<Diff> diffs = new ArrayList<Diff>();

        boolean remotelyPublished = targetNode.isNodeType("jmix:remotelyPublished");

        if (!remotelyPublished) {

            final ListOrderedMap targetUuids = getChildEntries(targetNode, targetNode.getSession());
            final ListOrderedMap sourceUuids = getChildEntries(sourceNode, sourceNode.getSession());

            if (!targetUuids.values().equals(sourceUuids.values())) {
                for (Iterator<?> iterator = sourceUuids.keySet().iterator(); iterator.hasNext();) {
                    String key = (String) iterator.next();
                    if (targetUuids.containsKey(key) && !targetUuids.get(key).equals(sourceUuids.get(key))) {
                        diffs.add(new ChildRenamedDiff(key, addPath(basePath, (String) targetUuids.get(key)),
                                addPath(basePath, (String) sourceUuids.get(key))));
                    }
                }
            }

            // Child nodes
            if (!targetUuids.keyList().equals(sourceUuids.keyList())) {

                @SuppressWarnings("unchecked")
                List<String> addedUuids = new ArrayList<String>(sourceUuids.keySet());
                addedUuids.removeAll(targetUuids.keySet());
                @SuppressWarnings("unchecked")
                List<String> removedUuids = new ArrayList<String>(targetUuids.keySet());
                removedUuids.removeAll(sourceUuids.keySet());

                // Ordering
                if (targetNode.getPrimaryNodeType().hasOrderableChildNodes()) {
                    Map<String, String> newOrdering = getOrdering(sourceUuids, Collections.<String>emptyList());
                    @SuppressWarnings("unchecked")
                    List<String> oldUuidsList = new ArrayList<String>(targetUuids.keySet());
                    oldUuidsList.removeAll(removedUuids);
                    @SuppressWarnings("unchecked")
                    List<String> newUuidsList = new ArrayList<String>(sourceUuids.keySet());
                    newUuidsList.removeAll(addedUuids);
                    if (!oldUuidsList.equals(newUuidsList)) {
                        for (int i = 1; i < oldUuidsList.size(); i++) {
                            String x = oldUuidsList.get(i);
                            int j = i;
                            while (j > 0 && sourceUuids.indexOf(oldUuidsList.get(j - 1)) > sourceUuids.indexOf(x)) {
                                oldUuidsList.set(j, oldUuidsList.get(j - 1));
                                j--;
                            }
                            if (j != i) {
                                String orderBeforeUuid = (j + 1 == oldUuidsList.size()) ? null
                                        : oldUuidsList.get(j + 1);
                                diffs.add(new ChildNodeReorderedDiff(x, orderBeforeUuid,
                                        addPath(basePath, (String) sourceUuids.get(x)),
                                        (String) sourceUuids.get(orderBeforeUuid), newOrdering));
                                logger.debug("reorder " + sourceUuids.get(x) + " before "
                                        + sourceUuids.get(orderBeforeUuid));
                                oldUuidsList.set(j, x);
                            }
                        }
                    }
                }

                // Removed nodes
                for (String removedUuid : removedUuids) {
                    try {
                        this.sourceNode.getSession().getNodeByUUID(removedUuid);
                    } catch (ItemNotFoundException e) {
                        // Item has been moved
                        diffs.add(new ChildRemovedDiff(removedUuid,
                                addPath(basePath, (String) targetUuids.get(removedUuid)), removedUuid));
                    }
                }

                // Added nodes
                for (String addedUuid : addedUuids) {
                    diffs.add(new ChildAddedDiff(addedUuid, addPath(basePath, (String) sourceUuids.get(addedUuid)),
                            addedUuid.equals(sourceUuids.lastKey()) ? null
                                    : (String) sourceUuids
                                            .get(sourceUuids.get(sourceUuids.indexOf(addedUuid) + 1))));
                }
            }
        }

        PropertyIterator targetProperties = targetNode.getProperties();

        while (targetProperties.hasNext()) {

            JCRPropertyWrapper targetProperty = (JCRPropertyWrapper) targetProperties.next();

            String propertyName = targetProperty.getName();
            if (IGNORED_PROPRTIES.contains(propertyName)) {
                continue;
            }

            if (!sourceNode.hasProperty(propertyName)) {
                if (targetProperty.isMultiple()) {
                    Value[] values = targetProperty.getRealValues();
                    for (Value value : values) {
                        diffs.add(new PropertyRemovedDiff(addPath(basePath, propertyName), value));
                    }
                } else {
                    diffs.add(new PropertyChangedDiff(addPath(basePath, propertyName), null));
                }
            } else {

                JCRPropertyWrapper sourceProperty = sourceNode.getProperty(propertyName);

                if (targetProperty.isMultiple() != sourceProperty.isMultiple()) {
                    throw new RepositoryException();
                }

                if (targetProperty.isMultiple()) {

                    List<? extends Value> targetValues = Arrays.asList(targetProperty.getRealValues());
                    List<? extends Value> sourceValues = Arrays.asList(sourceProperty.getRealValues());

                    Map<String, Value> addedValues = new HashMap<String, Value>();
                    for (Value value : sourceValues) {
                        addedValues.put(value.getString(), value);
                    }
                    for (Value value : targetValues) {
                        addedValues.remove(value.getString());
                    }
                    for (Value value : addedValues.values()) {
                        diffs.add(new PropertyAddedDiff(addPath(basePath, propertyName), value));
                    }

                    Map<String, Value> removedValues = new HashMap<String, Value>();
                    for (Value value : targetValues) {
                        removedValues.put(value.getString(), value);
                    }
                    for (Value value : sourceValues) {
                        removedValues.remove(value.getString());
                    }
                    for (Value value : removedValues.values()) {
                        diffs.add(new PropertyRemovedDiff(addPath(basePath, propertyName), value));
                    }
                } else {
                    if (!equalsValue(targetProperty.getRealValue(), sourceProperty.getRealValue())) {
                        diffs.add(new PropertyChangedDiff(addPath(basePath, propertyName),
                                sourceProperty.getRealValue()));
                    }
                }
            }
        }

        PropertyIterator sourceProperties = sourceNode.getProperties();

        while (sourceProperties.hasNext()) {

            JCRPropertyWrapper sourceProperty = (JCRPropertyWrapper) sourceProperties.next();

            String propertyName = sourceProperty.getName();

            if (IGNORED_PROPRTIES.contains(propertyName)) {
                continue;
            }
            if (!targetNode.hasProperty(propertyName)) {
                if (sourceProperty.isMultiple()) {
                    Value[] values = sourceProperty.getRealValues();
                    for (Value value : values) {
                        diffs.add(new PropertyAddedDiff(addPath(basePath, sourceProperty.getName()), value));
                    }
                } else {
                    diffs.add(new PropertyChangedDiff(addPath(basePath, sourceProperty.getName()),
                            sourceProperty.getRealValue()));
                }
            }

        }

        for (Diff diff : new ArrayList<Diff>(diffs)) {
            if (diff instanceof PropertyAddedDiff
                    && ((PropertyAddedDiff) diff).propertyPath.endsWith(Constants.JCR_MIXINTYPES)) {
                diffs.remove(diff);
                diffs.add(0, diff);
            }
        }

        for (Diff diff : new ArrayList<Diff>(diffs)) {
            if (diff instanceof PropertyRemovedDiff
                    && ((PropertyRemovedDiff) diff).propertyPath.endsWith(Constants.JCR_MIXINTYPES)) {
                diffs.remove(diff);
                diffs.add(0, diff);
            }
        }

        if (!remotelyPublished) {
            NodeIterator targetSubNodes = targetNode.getNodes();
            while (targetSubNodes.hasNext()) {
                JCRNodeWrapper targetSubNode = (JCRNodeWrapper) targetSubNodes.next();
                if (sourceNode.hasNode(targetSubNode.getName()) && !targetSubNode.isVersioned()
                        && !sourceNode.getNode(targetSubNode.getName()).isVersioned()
                        && JCRPublicationService.supportsPublication(targetSubNode.getSession(), targetSubNode)) {
                    diffs.addAll(compare(sourceNode.getNode(targetSubNode.getName()), targetSubNode,
                            addPath(basePath, targetSubNode.getName())));
                }
            }
        }

        return diffs;
    }

    private Map<String, String> getOrdering(ListOrderedMap uuids1, List<String> removed) {
        Map<String, String> previousMap = new LinkedHashMap<String, String>();
        ListIterator<?> it = uuids1.keyList().listIterator(uuids1.size());
        String previous = "";
        while (it.hasPrevious()) {
            String uuid = (String) it.previous();
            if (!removed.contains(uuid)) {
                previousMap.put(uuid, previous);
                previous = uuid;
            }
        }
        return previousMap;
    }

    private ListOrderedMap getChildEntries(JCRNodeWrapper node, JCRSessionWrapper session)
            throws RepositoryException {
        ListOrderedMap childEntries = new ListOrderedMap();
        for (JCRNodeWrapper child : node.getNodes()) {
            try {
                if (!child.isNodeType("jmix:nolive") && JCRPublicationService.supportsPublication(session, child)
                        && !(session.getWorkspace().getName().equals("live") && child.hasProperty("j:originWS")
                                && child.getProperty("j:originWS").getString().equals("live"))) {
                    session.getNodeByUUID(child.getIdentifier());
                    childEntries.put(child.getIdentifier(), child.getName());
                }
            } catch (ItemNotFoundException e) {
                // item does not exist in this workspace
            }
        }
        return childEntries;
    }

    public boolean equalsValue(Value o1, Value o2) {
        try {
            if (o1.getType() != o2.getType()) {
                return false;
            }
            if (o1.getType() == PropertyType.BINARY) {
                return o1.equals(o2);
            } else {
                return o1.getString().equals(o2.getString());
            }
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }

    private String addPath(String basePath, String name) {
        return basePath.equals("") ? name : basePath + "/" + name;
    }

    private JCRNodeWrapper getParentTarget(JCRNodeWrapper target, String path) throws RepositoryException {
        if (path.contains("/")) {
            return target.getNode(StringUtils.substringBeforeLast(path, "/"));
        } else {
            return target;
        }
    }

    private String getTargetName(String path) {
        if (path.contains("/")) {
            return StringUtils.substringAfterLast(path, "/");
        } else {
            return path;
        }
    }

    interface Diff {

        boolean apply() throws RepositoryException;
    }

    class ChildRenamedDiff implements Diff {

        private String uuid;
        private String oldName;
        private String newName;

        ChildRenamedDiff(String uuid, String oldName, String newName) {
            this.uuid = uuid;
            this.oldName = oldName;
            this.newName = newName;
        }

        @Override
        public boolean apply() throws RepositoryException {
            return !(targetNode.hasNode(oldName) && !targetNode.getNode(oldName).isVersioned())
                    || targetNode.getNode(oldName).rename(newName);
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            ChildRenamedDiff that = (ChildRenamedDiff) o;

            if (!newName.equals(that.newName)) {
                return false;
            }
            if (!oldName.equals(that.oldName)) {
                return false;
            }
            if (!uuid.equals(that.uuid)) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = uuid.hashCode();
            result = 31 * result + oldName.hashCode();
            result = 31 * result + newName.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "ChildRenamedDiff{" + "uuid='" + uuid + '\'' + ", oldName='" + oldName + '\'' + ", newName='"
                    + newName + '\'' + '}';
        }
    }

    class ChildAddedDiff implements Diff {

        private String uuid;
        private String newName;
        private String nextSibling;

        ChildAddedDiff(String uuid, String newName, String nextSibling) {
            this.uuid = uuid;
            this.newName = newName;
            this.nextSibling = nextSibling;
        }

        @Override
        public boolean apply() throws RepositoryException {

            if (sourceNode.getNode(newName).isVersioned() || targetNode.hasNode(newName)) {
                if (targetNode.hasNode(newName) && (nextSibling == null || targetNode.hasNode(nextSibling))
                        && targetNode.getPrimaryNodeType().hasOrderableChildNodes()) {
                    if (!newName.contains("/") && (nextSibling == null || !nextSibling.contains("/"))) {
                        // todo reorder non-versionable sub nodes
                        targetNode.orderBefore(newName, nextSibling);
                    }
                    targetNode.getSession().save();
                }
                return true;
            }

            JCRNodeWrapper targetNode = getParentTarget(ConflictResolver.this.targetNode, newName);
            JCRNodeWrapper sourceNode = getParentTarget(ConflictResolver.this.sourceNode, newName);
            String newNameParsed = getTargetName(newName);

            targetNode.getSession().save();
            JCRPublicationService.getInstance().doClone(sourceNode.getNode(newNameParsed), sourceNode.getSession(),
                    targetNode.getSession(), toCheckpoint);
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || this.getClass() != o.getClass()) {
                return false;
            }

            ChildAddedDiff that = (ChildAddedDiff) o;

            if (newName != null ? !newName.equals(that.newName) : that.newName != null) {
                return false;
            }
            if (nextSibling != null ? !nextSibling.equals(that.nextSibling) : that.nextSibling != null) {
                return false;
            }
            if (uuid != null ? !uuid.equals(that.uuid) : that.uuid != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = uuid != null ? uuid.hashCode() : 0;
            result = 31 * result + (newName != null ? newName.hashCode() : 0);
            result = 31 * result + (nextSibling != null ? nextSibling.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "ChildAddedDiff{" + "uuid='" + uuid + '\'' + ", newName='" + newName + '\'' + ", nextSibling='"
                    + nextSibling + '\'' + '}';
        }
    }

    class ChildRemovedDiff implements Diff {

        private String uuid;
        private String oldName;
        private String identifier;

        ChildRemovedDiff(String uuid, String oldName, String identifier) {
            this.uuid = uuid;
            this.oldName = oldName;
            this.identifier = identifier;
        }

        @Override
        public boolean apply() throws RepositoryException {
            if (targetNode.hasNode(oldName)) {
                final JCRNodeWrapper node = targetNode.getNode(oldName);
                if (node.getIdentifier().equals(identifier)) {
                    JCRPublicationService.getInstance().addRemovedLabel(node,
                            node.getSession().getWorkspace().getName() + "_removed_at_"
                                    + JCRVersionService.DATE_FORMAT.print(System.currentTimeMillis()));
                    node.remove();
                }
            }
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            ChildRemovedDiff that = (ChildRemovedDiff) o;

            if (!oldName.equals(that.oldName)) {
                return false;
            }
            if (!uuid.equals(that.uuid)) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = uuid.hashCode();
            result = 31 * result + oldName.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "ChildRemovedDiff{" + "uuid='" + uuid + '\'' + ", oldName='" + oldName + '\'' + '}';
        }
    }

    class ChildNodeReorderedDiff implements Diff {

        private String name;
        private String orderBeforeName;
        private String uuid;
        private String orderBeforeUuid;
        private Map<String, String> ordering;

        ChildNodeReorderedDiff(String uuid, String orderBeforeUuid, String name, String orderBeforeName,
                Map<String, String> ordering) {
            this.name = name;
            this.orderBeforeName = orderBeforeName;
            this.uuid = uuid;
            this.orderBeforeUuid = orderBeforeUuid;
            this.ordering = ordering;
        }

        @Override
        public boolean apply() throws RepositoryException {
            if (targetNode.hasNode(name)) {
                JCRNodeWrapper realTargetNode = targetNode;
                String realName = name;
                if (name.contains("/")) {
                    realTargetNode = targetNode.getNode(StringUtils.substringBeforeLast(name, "/"));
                    realName = StringUtils.substringAfterLast(name, "/");
                }
                if (realTargetNode.getPrimaryNodeType().hasOrderableChildNodes()) {

                    while (orderBeforeName != null && !realTargetNode.hasNode(orderBeforeName)) {
                        orderBeforeUuid = ordering.get(orderBeforeUuid);
                        try {
                            if (orderBeforeUuid.equals("")) {
                                orderBeforeName = null;
                            } else {
                                orderBeforeName = realTargetNode.getSession().getNodeByUUID(orderBeforeUuid)
                                        .getName();
                            }
                        } catch (ItemNotFoundException e) {
                            // ignored exception
                        }
                    }
                    realTargetNode.orderBefore(realName, orderBeforeName);
                }
            }
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            ChildNodeReorderedDiff that = (ChildNodeReorderedDiff) o;

            if (orderBeforeUuid != null ? !orderBeforeUuid.equals(that.orderBeforeUuid)
                    : that.orderBeforeUuid != null) {
                return false;
            }
            if (uuid != null ? !uuid.equals(that.uuid) : that.uuid != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = uuid != null ? uuid.hashCode() : 0;
            result = 31 * result + (orderBeforeUuid != null ? orderBeforeUuid.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "ChildNodeReorderedDiff{" + "name='" + name + '\'' + ", orderBeforeName='" + orderBeforeName
                    + '\'' + ", uuid='" + uuid + '\'' + ", orderBeforeUuid='" + orderBeforeUuid + '\''
                    + ", ordering=" + ordering + '}';
        }
    }

    class PropertyAddedDiff implements Diff {

        private String propertyPath;
        private Value newValue;

        PropertyAddedDiff(String propertyPath, Value newValue) {
            this.propertyPath = propertyPath;
            this.newValue = newValue;
        }

        @Override
        public boolean apply() throws RepositoryException {

            JCRNodeWrapper targetNode = getParentTarget(ConflictResolver.this.targetNode, propertyPath);
            String propertyName = getTargetName(propertyPath);

            if (!targetNode.isCheckedOut()) {
                targetNode.checkout();
            }
            if (propertyName.equals(Constants.JCR_MIXINTYPES)) {
                targetNode.addMixin(newValue.getString());
            } else if (targetNode.hasProperty(propertyName)) {
                List<Value> values = new ArrayList<Value>(
                        Arrays.asList(targetNode.getProperty(propertyName).getRealValues()));
                values.add(newValue);
                targetNode.setProperty(propertyName, values.toArray(new Value[values.size()]));
            } else {
                targetNode.setProperty(propertyName, new Value[] { newValue });
            }
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            PropertyAddedDiff that = (PropertyAddedDiff) o;

            if (!equalsValue(newValue, that.newValue)) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = propertyPath != null ? propertyPath.hashCode() : 0;
            result = 31 * result + (newValue != null ? newValue.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "PropertyAddedDiff{" + "propertyPath='" + propertyPath + '\'' + ", newValue=" + newValue + '}';
        }
    }

    class PropertyRemovedDiff implements Diff {

        private String propertyPath;
        private Value oldValue;

        PropertyRemovedDiff(String propertyPath, Value oldValue) {
            this.propertyPath = propertyPath;
            this.oldValue = oldValue;
        }

        @Override
        public boolean apply() throws RepositoryException {

            try {

                JCRNodeWrapper targetNode = getParentTarget(ConflictResolver.this.targetNode, propertyPath);
                String propertyName = getTargetName(propertyPath);

                if (!targetNode.isCheckedOut()) {
                    targetNode.checkout();
                }

                if (propertyName.equals(Constants.JCR_MIXINTYPES)) {
                    targetNode.removeMixin(oldValue.getString());
                } else {
                    List<? extends Value> oldValues = Arrays
                            .asList(targetNode.getProperty(propertyName).getRealValues());
                    List<Value> newValues = new ArrayList<Value>();
                    for (Value value : oldValues) {
                        if (!equalsValue(value, oldValue)) {
                            newValues.add(value);
                        }
                    }
                    if (newValues.isEmpty()) {
                        targetNode.getProperty(propertyName).remove();
                    } else {
                        targetNode.setProperty(propertyName, newValues.toArray(new Value[newValues.size()]));
                    }
                }
            } catch (PathNotFoundException e) {
                logger.debug("Property has already been removed", e);
            }
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            PropertyRemovedDiff that = (PropertyRemovedDiff) o;

            if (!equalsValue(oldValue, that.oldValue)) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = propertyPath != null ? propertyPath.hashCode() : 0;
            result = 31 * result + (oldValue != null ? oldValue.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "PropertyRemovedDiff{" + "propertyPath='" + propertyPath + '\'' + ", oldValue=" + oldValue + '}';
        }
    }

    class PropertyChangedDiff implements Diff {

        private String propertyPath;
        private Value newValue;

        PropertyChangedDiff(String propertyPath, Value newValue) {
            this.propertyPath = propertyPath;
            this.newValue = newValue;
        }

        @Override
        public boolean apply() throws RepositoryException {

            JCRNodeWrapper targetNode = getParentTarget(ConflictResolver.this.targetNode, propertyPath);
            String propertyName = getTargetName(propertyPath);

            if (!targetNode.isCheckedOut()) {
                targetNode.checkout();
            }

            if (newValue == null) {
                if (targetNode.hasProperty(propertyName)) {
                    targetNode.getProperty(propertyName).remove();
                }
            } else {
                targetNode.getRealNode().setProperty(propertyName, newValue);
            }
            return true;
        }

        @Override
        public boolean equals(Object o) {

            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            PropertyChangedDiff that = (PropertyChangedDiff) o;

            if (newValue != null ? !newValue.equals(that.newValue) : that.newValue != null) {
                return false;
            }
            if (propertyPath != null ? !propertyPath.equals(that.propertyPath) : that.propertyPath != null) {
                return false;

            }

            return true;
        }

        @Override
        public int hashCode() {
            int result = propertyPath != null ? propertyPath.hashCode() : 0;
            result = 31 * result + (newValue != null ? newValue.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "PropertyChangedDiff{" + "propertyPath='" + propertyPath + '\'' + ", newValue=" + newValue + '}';
        }
    }
}