com.flexive.shared.content.FxDelta.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.shared.content.FxDelta.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library 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.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.shared.content;

import com.flexive.shared.FxDiff;
import com.flexive.shared.exceptions.FxInvalidParameterException;
import com.flexive.shared.exceptions.FxNotFoundException;
import com.flexive.shared.structure.FxPropertyAssignment;
import com.flexive.shared.value.FxValue;
import org.apache.commons.lang.ArrayUtils;

import java.io.Serializable;
import java.util.*;

/**
 * Compute Delta changes for FxContents
 *
 * @author Markus Plesser (markus.plesser@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 */
public class FxDelta implements Serializable {
    private static final long serialVersionUID = -6246822483703676822L;
    private List<FxPropertyData> flatData;

    /**
     * A single delta change
     */
    public static class FxDeltaChange implements Serializable {
        public static enum ChangeType {
            Add, Remove, Update, None
        }

        private static final long serialVersionUID = -9026076539541708345L;

        private String XPath;
        private FxData originalData;
        private FxData newData;
        private boolean positionChange;
        private boolean dataChange;
        private boolean property;
        private boolean languageSettingChanged;
        private int retryCount;
        private boolean multiColumn;
        private ChangeType changeType;
        private boolean flatStorageChange;

        /**
         * Ctor
         *
         * @param changeType   type of change
         * @param XPath        affected XPath
         * @param originalData original data
         * @param newData      new data
         */
        @SuppressWarnings({ "ConstantConditions" })
        public FxDeltaChange(ChangeType changeType, String XPath, FxData originalData, FxData newData) {
            this.changeType = changeType;
            this.XPath = XPath;
            this.originalData = originalData;
            this.newData = newData;
            this.property = originalData instanceof FxPropertyData || newData instanceof FxPropertyData;
            if (newData == null || originalData == null) {
                this.positionChange = true;
                this.dataChange = true;
            } else {
                positionChange = newData.getPos() != originalData.getPos();
                if (property) {
                    final FxValue origValue = ((FxPropertyData) originalData).getValue();
                    final FxValue newValue = ((FxPropertyData) newData).getValue();
                    this.dataChange = !origValue.equals(newValue) && (!origValue.isEmpty() || !newValue.isEmpty());
                } else {
                    this.dataChange = false;
                }
            }
            this.languageSettingChanged = this.dataChange && this.property;
            if (this.languageSettingChanged && this.originalData != null && this.newData != null) {
                FxValue ov = ((FxPropertyData) originalData).getValue();
                FxValue nv = ((FxPropertyData) newData).getValue();
                this.languageSettingChanged = !ArrayUtils.isEquals(ov.getTranslatedLanguages(),
                        nv.getTranslatedLanguages());
            }
            if (this.property && this.newData != null) {
                switch (((FxPropertyAssignment) newData.getAssignment()).getProperty().getDataType()) {
                case SelectMany:
                    this.multiColumn = true;
                    break;
                default:
                    this.multiColumn = false;
                }
            } else
                this.multiColumn = false;
            if (!isProperty())
                this.flatStorageChange = false;
            else {
                FxData data = (getNewData() != null ? getNewData() : getOriginalData());
                this.flatStorageChange = data != null
                        && ((FxPropertyData) data).getPropertyAssignment().isFlatStorageEntry();
            }
        }

        /**
         * Get the type of change
         *
         * @return type of change
         */
        public ChangeType getChangeType() {
            return changeType;
        }

        /**
         * Getter for the XPath (based on the origina XPath)
         *
         * @return XPath
         */
        public String getXPath() {
            return XPath;
        }

        /**
         * Getter for the original data
         *
         * @return original data
         */
        public FxData getOriginalData() {
            return originalData;
        }

        /**
         * Getter for the new data
         *
         * @return new data
         */
        public FxData getNewData() {
            return newData;
        }

        /**
         * Is the change affecting a property?
         *
         * @return property affected?
         */
        public boolean isProperty() {
            return property;
        }

        /**
         * Is this a system internal property like version or step?
         *
         * @return is system internal
         */
        public boolean isInternal() {
            if (originalData != null)
                return originalData.isSystemInternal();
            return newData != null && newData.isSystemInternal();
        }

        /**
         * Is this a position change only?
         *
         * @return position change only?
         */
        public boolean isPositionChangeOnly() {
            return positionChange && !dataChange;
        }

        /**
         * Is the change affecting a group?
         *
         * @return group affected?
         */
        public boolean isGroup() {
            return !property;
        }

        /**
         * Did the position change?
         *
         * @return position change
         */
        public boolean isPositionChange() {
            return positionChange;
        }

        /**
         * Has data changed?
         *
         * @return data changed
         */
        public boolean isDataChange() {
            return dataChange;
        }

        /**
         * Have language settings changed? (New or removed translations,etc)
         *
         * @return language settings changed
         */
        public boolean isLanguageSettingChanged() {
            return languageSettingChanged;
        }

        /**
         * Does this change affect a flat storage entry?
         *
         * @return change affects a flat storage entry
         */
        public boolean isFlatStorageChange() {
            return flatStorageChange;
        }

        /**
         * <b>Internal use only!</b>
         * Is this delta updateable or do we need delete/insert?
         * A property is not updateable if language settings changed or it spans multiple columns like a select-many
         *
         * @return if this chance is updateable
         */
        public boolean _isUpdateable() {
            return !(languageSettingChanged || multiColumn);
        }

        /**
         * <b>Internal use only!</b>
         * Increase the retry count - used when saving and conflicts arise that
         * are fixed after some retries (ie positioning changes)
         */
        public synchronized void _increaseRetries() {
            retryCount++;
        }

        /**
         * <b>Internal use only!</b>
         * Getter for the retry count
         *
         * @return retry count
         */
        public synchronized int _getRetryCount() {
            return retryCount;
        }

        /**
         * {@inheritDoc}
         */
        public String toString() {
            return XPath + ": " + changeType.name() + (isDataChange() ? " DATA" : "")
                    + (isPositionChange() ? " POS" : "") + (languageSettingChanged ? " LANG" : "");
        }
    }

    private final static List<FxDeltaChange> EMPTY = Collections.unmodifiableList(new ArrayList<FxDeltaChange>(0));

    private List<FxDeltaChange> updates, adds, removes;
    private boolean internalPropertyChanged = false;
    private boolean onlyInternalPropertyChanges = true;

    /**
     * Ctor
     *
     * @param updates delta updates
     * @param adds    delta adds
     * @param removes delta removes
     */
    public FxDelta(List<FxDeltaChange> updates, List<FxDeltaChange> adds, List<FxDeltaChange> removes) {
        this.updates = (updates != null ? updates : EMPTY);
        this.adds = (adds != null ? adds : EMPTY);
        this.removes = (removes != null ? removes : EMPTY);
        checkInternal(this.updates);
        if (!internalPropertyChanged)
            checkInternal(this.adds);
        if (!internalPropertyChanged)
            checkInternal(this.removes);
        this.flatData = new ArrayList<FxPropertyData>(20);
        for (FxDeltaChange up : this.updates) {
            if (up.isFlatStorageChange())
                this.flatData.add((FxPropertyData) up.getNewData());
        }
        for (FxDeltaChange add : this.adds) {
            if (add.isFlatStorageChange())
                this.flatData.add((FxPropertyData) add.getNewData());
        }
    }

    private void checkInternal(List<FxDeltaChange> list) {
        for (FxDeltaChange c : list)
            if (c.isProperty() && ((c.getOriginalData() != null && c.getOriginalData().isSystemInternal())
                    || (c.getNewData() != null && c.getNewData().isSystemInternal()))) {
                internalPropertyChanged = true;
                if (!onlyInternalPropertyChanges)
                    break; //only break if we know that "other" properties changed as well
            } else
                onlyInternalPropertyChanges = false;
    }

    /**
     * Get a list of all updated deltas
     *
     * @return list of all updated deltas
     */
    public List<FxDeltaChange> getUpdates() {
        return updates;
    }

    /**
     * Get a list of all added deltas
     *
     * @return list of all added deltas
     */
    public List<FxDeltaChange> getAdds() {
        return adds;
    }

    /**
     * Get a list of all removed deltas
     *
     * @return list of all removed deltas
     */
    public List<FxDeltaChange> getRemoves() {
        return removes;
    }

    /**
     * Get all adds and updates that affect a flat storage
     *
     * @return list of all adds and updates that affect a flat storage
     */
    public List<FxPropertyData> getFlatStorageAddsUpdates() {
        return flatData;
    }

    /**
     * Was there a change of system internal properties like ACL?
     *
     * @return change of system internal properies
     */
    public boolean isInternalPropertyChanged() {
        return internalPropertyChanged;
    }

    /**
     * Are there only internal property changes (like step, version) and no data changes?
     *
     * @return only internal property changes
     */
    public boolean isOnlyInternalPropertyChanges() {
        return onlyInternalPropertyChanges;
    }

    /**
     * Are there any changes?
     *
     * @return changes?
     */
    public boolean changes() {
        return !(updates.size() == 0 && removes.size() == 0 && adds.size() == 0);
    }

    /**
     * Are there any data (and not only position) changed?
     *
     * @return data changes?
     * @since 3.1.5
     */
    public boolean isDataChanged() {
        if (!changes())
            return false;
        if (adds.size() > 0 || removes.size() > 0)
            return true;
        for (FxDelta.FxDeltaChange change : updates)
            if (change.isDataChange())
                return true;
        return false;
    }

    /**
     * Are there any changes relating to groups or group positions?
     *
     * @return  group changes?
     * @since 3.2.0
     */
    public boolean isGroupDataChanged() {
        return containsGroupChanges(adds) || containsGroupChanges(removes) || containsGroupChanges(updates);
    }

    private boolean containsGroupChanges(List<FxDeltaChange> changes) {
        for (FxDeltaChange change : changes) {
            if (change.isGroup()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Create a dump of all changes for debugging purposes
     *
     * @return dump of all changes for debugging purposes
     */
    public String dump() {
        if (!changes())
            return "===> No changes! <===";
        StringBuilder sb = new StringBuilder(1000);
        sb.append("<=== changes start ===>\n");
        if (updates.size() > 0) {
            sb.append("Updates:\n");
            for (FxDeltaChange c : updates) {
                if (c.isPositionChange() && !c.isDataChange()) {
                    sb.append(c.getXPath()).append(": Position changed from ").append(c.getOriginalData().getPos())
                            .append(" to ").append(c.getNewData().getPos()).append("\n");
                } else if (c.isDataChange()) {
                    sb.append(c.getXPath()).append(": ");
                    if (c.isPositionChange())
                        sb.append("Position changed from ").append(c.getOriginalData().getPos()).append(" to ")
                                .append(c.getNewData().getPos());
                    if (c.isDataChange())
                        sb.append(" [data changes]");
                    if (c._isUpdateable())
                        sb.append(" [lang.settings changes]");
                    sb.append("\n");
                }
            }
        }
        if (adds.size() > 0) {
            sb.append("Adds:\n");
            for (FxDeltaChange c : adds) {
                sb.append(c.getXPath()).append(": added at position ").append(c.getNewData().getPos()).append("\n");
            }
        }
        if (removes.size() > 0) {
            sb.append("Removes:\n");
            for (FxDeltaChange c : removes) {
                sb.append(c.getXPath()).append(": removed at position ").append(c.getOriginalData().getPos())
                        .append("\n");
            }
        }
        sb.append("<=== changes end ===>\n");

        return sb.toString();
    }

    /**
     * Get all changes from <i>original</i> to <i>compare</i> in correct order (for display in UI's)
     *
     * @param original original content
     * @param compare  content that original is compared against
     * @return differences
     */
    public List<FxDeltaChange> getDiff(final FxContent original, final FxContent compare) {
        //This is not performant but since it should be rarely used in timecritical environments we don't care for now...
        List<FxDeltaChange> ret = new ArrayList<FxDeltaChange>(10);
        Map<String, FxDeltaChange> cache = new HashMap<String, FxDeltaChange>(
                removes.size() + adds.size() + updates.size());
        for (FxDeltaChange a : adds)
            cache.put(a.getXPath(), a);
        for (FxDeltaChange r : removes)
            cache.put(r.getXPath(), r);
        for (FxDeltaChange u : updates)
            cache.put(u.getXPath(), u);

        List<String> org = original.getAllXPaths("/");
        List<String> comp = compare.getAllXPaths("/");
        FxDiff diff = new FxDiff(org, comp);
        List<FxDiff.Difference> ld = diff.diff();

        /*System.out.println("== ORG ==");
        int i = 0;
        for (String s : org)
        System.out.println((i++) + ": " + s);
        System.out.println("== COMP ==");
        i = 0;
        for (String s : comp)
        System.out.println((i++) + ": " + s);
            
        System.out.println("== DELTA ==");
        for (FxDeltaChange c : cache.values())
        System.out.println(c);
        System.out.println("== DIFF ==");*/
        int checked = 0; //index of last checked update
        for (FxDiff.Difference d : ld) {
            //            System.out.println(d);
            if (d.isDelete()) {
                for (int pos = d.getDeletedStart(); pos <= d.getDeletedEnd(); pos++) {
                    String delXP = org.get(pos);
                    //                    System.out.println("Deleted: " + delXP);
                    if (cache.containsKey(delXP)) {
                        //check for any XPath "before" entry for a data change
                        checked = checkUpdated(d, ret, cache, org, checked);
                        ret.add(cache.remove(delXP));
                    }
                }
            }
            if (d.isAdd()) {
                for (int pos = d.getAddedStart(); pos <= d.getAddedEnd(); pos++) {
                    String addXP = comp.get(pos);
                    //                    System.out.println("Added: " + addXP);
                    if (cache.containsKey(addXP)) {
                        //check for any XPath "before" entry for a data change
                        checked = checkUpdated(d, ret, cache, org, checked);
                        ret.add(cache.remove(addXP));
                    }
                }
            }
        }
        checkUpdated(null, ret, cache, org, checked);
        ret.removeAll(Collections.singleton(null)); //remove null entries
        return ret;
    }

    private int checkUpdated(FxDiff.Difference d, List<FxDeltaChange> ret, Map<String, FxDeltaChange> cache,
            List<String> org, int checked) {
        if (checked >= org.size())
            return checked;
        int end = d == null ? org.size() : (d.isAdd() ? d.getAddedEnd() : d.getDeletedEnd() - 1);
        if (end > org.size())
            end = org.size();
        for (int i = 0; i < end; i++) {
            if (cache.containsKey(org.get(i))) {
                ret.add(cache.remove(org.get(i)));
                //                System.out.println("Adding update: " + org.get(i));
            }
        }
        return checked;
    }

    /**
     * Compare <code>original</code> to <code>compare</code> FxContent.
     * Both contents should be compacted and empty entries removed for correct results.
     * Flatstorage entries will only be added to the remove list.
     *
     * @param original original content
     * @param compare  content that original is compared against
     * @return deltas
     * @throws FxInvalidParameterException on errors
     * @throws FxNotFoundException         on errors
     */
    public static FxDelta processDelta(final FxContent original, final FxContent compare)
            throws FxNotFoundException, FxInvalidParameterException {
        List<String> org = original.getAllXPaths("/");
        List<String> comp = compare.getAllXPaths("/");

        List<FxDeltaChange> updates = null, adds = null, removes = null;

        //remove all xpaths from comp that are not contained in org => updates
        List<String> update = new ArrayList<String>(comp);
        update.retainAll(org);
        for (String xp : update) {
            if (xp.endsWith("/")) {
                //group
                xp = xp.substring(0, xp.length() - 1);
                if (!compare.getGroupData(xp).equals(original.getGroupData(xp))) {
                    if (updates == null)
                        updates = new ArrayList<FxDeltaChange>(10);
                    updates.add(new FxDeltaChange(FxDeltaChange.ChangeType.Update, xp, original.getGroupData(xp),
                            compare.getGroupData(xp)));
                }
            } else if (!compare.getData(xp).equals(original.getData(xp))) {
                //property
                if (updates == null)
                    updates = new ArrayList<FxDeltaChange>(10);
                final FxPropertyData orgData = original.getPropertyData(xp);
                updates.add(new FxDeltaChange(FxDeltaChange.ChangeType.Update, xp, orgData,
                        compare.getPropertyData(xp)));
            }
        }

        List<String> add = new ArrayList<String>(comp);
        add.removeAll(org);
        for (String xp : add) {
            if (adds == null)
                adds = new ArrayList<FxDeltaChange>(10);
            if (xp.endsWith("/")) {
                //group
                xp = xp.substring(0, xp.length() - 1);
                adds.add(new FxDeltaChange(FxDeltaChange.ChangeType.Add, xp, null, compare.getGroupData(xp)));
            } else {
                //property
                final FxPropertyData pdata = compare.getPropertyData(xp);
                adds.add(new FxDeltaChange(FxDeltaChange.ChangeType.Add, xp, null, pdata));
            }
        }

        List<String> rem = new ArrayList<String>(org);
        rem.removeAll(comp);
        for (String xp : rem) {
            if (removes == null)
                removes = new ArrayList<FxDeltaChange>(10);
            if (xp.endsWith("/")) {
                xp = xp.substring(0, xp.length() - 1);
                final FxGroupData groupData = original.getGroupData(xp);
                if (!groupData.isEmpty()) {
                    removes.add(new FxDeltaChange(FxDeltaChange.ChangeType.Remove, xp, groupData, null));
                }
            } else {
                final FxPropertyData propertyData = original.getPropertyData(xp);
                if (!propertyData.isEmpty()) {
                    removes.add(new FxDeltaChange(FxDeltaChange.ChangeType.Remove, xp, propertyData, null));
                }
            }
        }
        return new FxDelta(updates, adds, removes);
    }
}