/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage; import static org.nuxeo.ecm.core.storage.State.NOP; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArrayList; import org.nuxeo.ecm.core.api.model.Delta; import org.nuxeo.ecm.core.storage.State.ListDiff; import org.nuxeo.ecm.core.storage.State.StateDiff; import org.nuxeo.runtime.api.Framework; /** * Helpers for deep copy and deep diff of {@link State} objects. */ public class StateHelper { private static final String DISABLED_DELTA_PROP = "org.nuxeo.core.delta.disabled"; /** Utility class. */ private StateHelper() { } /** * Checks if we have a base type compatible with {@link State} helper processing. */ public static boolean isScalar(Object value) { return value instanceof String // || value instanceof Boolean // || value instanceof Long // || value instanceof Double // || value instanceof Calendar // || value instanceof Delta; } /** * Compares two values. */ public static boolean equalsStrict(Object a, Object b) { if (a == b) { return true; } else if (a == null || b == null) { return false; } else if (a instanceof State && b instanceof State) { return equalsStrict((State) a, (State) b); } else if (a instanceof List && b instanceof List) { @SuppressWarnings("unchecked") List<Serializable> la = (List<Serializable>) a; @SuppressWarnings("unchecked") List<Serializable> lb = (List<Serializable>) b; return equalsStrict(la, lb); } else if (a instanceof Object[] && b instanceof Object[]) { return equalsStrict((Object[]) a, (Object[]) b); } else if (a instanceof ListDiff && b instanceof ListDiff) { ListDiff lda = (ListDiff) a; ListDiff ldb = (ListDiff) b; return lda.isArray == ldb.isArray && equalsStrict(lda.diff, ldb.diff) && equalsStrict(lda.rpush, ldb.rpush); } else if (isScalar(a) && isScalar(b)) { return a.equals(b); } else { return false; } } /** * Compares two {@link State}s. */ public static boolean equalsStrict(State a, State b) { if (a == b) { return true; } if (a == null || b == null) { return false; } if (a.size() != b.size()) { return false; } if (!a.keySet().equals(b.keySet())) { return false; } for (Entry<String, Serializable> en : a.entrySet()) { String key = en.getKey(); Serializable va = en.getValue(); Serializable vb = b.get(key); if (!equalsStrict(va, vb)) { return false; } } return true; } /** * Compares two arrays of scalars. */ public static boolean equalsStrict(Object[] a, Object[] b) { // we have scalars, Arrays.equals() is enough return Arrays.equals(a, b); } /** * Compares two {@link List}s. */ public static boolean equalsStrict(List<Serializable> a, List<Serializable> b) { if (a == b) { return true; } if (a == null || b == null) { return false; } if (a.size() != b.size()) { return false; } for (Iterator<Serializable> ita = a.iterator(), itb = b.iterator(); ita.hasNext();) { if (!equalsStrict(ita.next(), itb.next())) { return false; } } return true; } /** * Compares two values. * <p> * A {@code null} value or an empty array or {@code List} is equivalent to an absent value. A {@code null} * {@link State} is equivalent to an empty {@link State} (or a {@link State} containing only absent values). */ public static boolean equalsLoose(Object a, Object b) { if (a == b) { return true; } else if (a instanceof State && b instanceof State // || a instanceof State && b == null // || a == null && b instanceof State) { return equalsLoose((State) a, (State) b); } else if (a instanceof List && b instanceof List // || a instanceof List && b == null // || a == null && b instanceof List) { @SuppressWarnings("unchecked") List<Serializable> la = (List<Serializable>) a; @SuppressWarnings("unchecked") List<Serializable> lb = (List<Serializable>) b; return equalsLoose(la, lb); } else if (a instanceof Object[] && b instanceof Object[] // || a instanceof Object[] && b == null // || a == null && b instanceof Object[]) { return equalsLoose((Object[]) a, (Object[]) b); } else if (a instanceof ListDiff && b instanceof ListDiff) { ListDiff lda = (ListDiff) a; ListDiff ldb = (ListDiff) b; return lda.isArray == ldb.isArray && equalsLoose(lda.diff, ldb.diff) && equalsLoose(lda.rpush, ldb.rpush); } else if (isScalar(a) && isScalar(b)) { return a.equals(b); } else { return false; } } /** * Compares two {@link State}s. * <p> * A {@code null} value or an empty array or {@code List} is equivalent to an absent value. A {@code null} * {@link State} is equivalent to an empty {@link State} (or a {@link State} containing only absent values). */ public static boolean equalsLoose(State a, State b) { if (a == null) { a = State.EMPTY; } if (b == null) { b = State.EMPTY; } for (Entry<String, Serializable> en : a.entrySet()) { Serializable va = en.getValue(); if (va == null) { // checked by loop on b continue; } String key = en.getKey(); Serializable vb = b.get(key); if (!equalsLoose(va, vb)) { return false; } } for (Entry<String, Serializable> en : b.entrySet()) { String key = en.getKey(); Serializable va = a.get(key); if (va != null) { // already checked by loop on a continue; } Serializable vb = en.getValue(); if (!equalsLoose(null, vb)) { return false; } } return true; } /** * Compares two arrays of scalars. * <p> * {@code null} values are equivalent to empty arrays. */ public static boolean equalsLoose(Object[] a, Object[] b) { if (a != null && a.length == 0) { a = null; } if (b != null && b.length == 0) { b = null; } // we have scalars, Arrays.equals() is enough return Arrays.equals(a, b); } /** * Compares two {@link List}s. * <p> * {@code null} values are equivalent to empty lists. */ public static boolean equalsLoose(List<Serializable> a, List<Serializable> b) { if (a != null && a.isEmpty()) { a = null; } if (b != null && b.isEmpty()) { b = null; } if (a == b) { return true; } if (a == null || b == null) { return false; } if (a.size() != b.size()) { return false; } for (Iterator<Serializable> ita = a.iterator(), itb = b.iterator(); ita.hasNext();) { if (!equalsLoose(ita.next(), itb.next())) { return false; } } return true; } /** * Makes a deep copy of a value. */ public static Serializable deepCopy(Object value) { return deepCopy(value, false); } /** * Makes a deep copy of a value, optionally thread-safe. * * @param threadSafe if {@code true}, then thread-safe datastructures are used */ public static Serializable deepCopy(Object value, boolean threadSafe) { if (value == null) { return (Serializable) value; } else if (value instanceof State) { return deepCopy((State) value, threadSafe); } else if (value instanceof List) { @SuppressWarnings("unchecked") List<Serializable> list = (List<Serializable>) value; return (Serializable) deepCopy(list, threadSafe); } else if (value instanceof Object[]) { // array values are supposed to be scalars return ((Object[]) value).clone(); } // else scalar value -- check anyway (debug) else if (!isScalar(value)) { throw new UnsupportedOperationException("Cannot deep copy: " + value.getClass().getName()); } return (Serializable) value; } /** * Makes a deep copy of a {@link State} map. */ public static State deepCopy(State state) { return deepCopy(state, false); } /** * Makes a deep copy of a {@link State} map, optionally thread-safe. * * @param threadSafe if {@code true}, then thread-safe datastructures are used */ public static State deepCopy(State state, boolean threadSafe) { State copy = new State(state.size(), threadSafe); for (Entry<String, Serializable> en : state.entrySet()) { copy.put(en.getKey(), deepCopy(en.getValue(), threadSafe)); } return copy; } /** * Makes a deep copy of a {@link List}. */ public static List<Serializable> deepCopy(List<Serializable> list) { return deepCopy(list, false); } /** * Makes a deep copy of a {@link List}, optionally thread-safe. * * @param threadSafe if {@code true}, then thread-safe datastructures are used */ public static List<Serializable> deepCopy(List<Serializable> list, boolean threadSafe) { List<Serializable> copy = threadSafe ? new CopyOnWriteArrayList<Serializable>() : new ArrayList<Serializable>( list.size()); for (Serializable v : list) { copy.add(deepCopy(v, threadSafe)); } return copy; } /** * Does a diff of two values. * * @return a {@link StateDiff}, a {@link ListDiff}, {@link #NOP}, or an actual value (including {@code null}) */ public static Serializable diff(Object a, Object b) { if (equalsLoose(a, b)) { return NOP; } if (a instanceof Object[] && b instanceof Object[]) { return diff((Object[]) a, (Object[]) b); } if (a instanceof List && b instanceof List) { @SuppressWarnings("unchecked") List<Object> la = (List<Object>) a; @SuppressWarnings("unchecked") List<Object> lb = (List<Object>) b; return (Serializable) diff(la, lb); } if (a instanceof State && b instanceof State) { StateDiff diff = diff((State) a, (State) b); return diff.isEmpty() ? NOP : diff; } return (Serializable) b; } public static Serializable diff(Object[] a, Object[] b) { List<Object> la = Arrays.asList(a); List<Object> lb = Arrays.asList(b); Serializable diff = diff(la, lb); if (diff instanceof List) { return b; } ListDiff listDiff = (ListDiff) diff; listDiff.isArray = true; return listDiff; } public static Serializable diff(List<Object> a, List<Object> b) { ListDiff listDiff = new ListDiff(); listDiff.isArray = false; int aSize = a.size(); int bSize = b.size(); // TODO configure zero-length "a" case boolean doRPush = aSize > 0 && aSize < bSize; // we can use a list diff if lists are the same size, // or we have a rpush boolean doDiff = aSize == bSize || doRPush; if (!doDiff) { return (Serializable) b; } int len = Math.min(aSize, bSize); List<Object> diff = new ArrayList<>(len); int nops = 0; int diffs = 0; for (int i = 0; i < len; i++) { Serializable elemDiff = diff(a.get(i), b.get(i)); if (elemDiff == NOP) { nops++; } else if (elemDiff instanceof StateDiff) { diffs++; } // TODO if the individual element diffs are big StateDiffs, // do a full State replacement instead diff.add(elemDiff); } if (nops == len) { // only nops diff = null; } else if (diffs == 0) { // only setting elements or nops // TODO use a higher ratio than 0% of diffs return (Serializable) b; } listDiff.diff = diff; if (doRPush) { List<Object> rpush = new ArrayList<>(bSize - aSize); for (int i = aSize; i < bSize; i++) { rpush.add(b.get(i)); } listDiff.rpush = rpush; } return listDiff; } /** * Makes a diff copy of two {@link State} maps. * <p> * The returned diff state contains only the key/values that changed. {@code null} values are equivalent to absent * values. * <p> * For values set to null or removed, the value is null. * <p> * When setting a delta, the old value is checked to know if the delta should be kept or if a full value should be * set instead. * <p> * For sub-documents, a recursive diff is returned. * * @return a {@link StateDiff} which, when applied to a, gives b. */ public static StateDiff diff(State a, State b) { StateDiff diff = new StateDiff(); for (Entry<String, Serializable> en : a.entrySet()) { Serializable va = en.getValue(); if (va == null) { // checked by loop on b continue; } String key = en.getKey(); Serializable vb = b.get(key); if (vb == null) { // value must be cleared diff.put(key, null); } else { // compare values Serializable elemDiff = diff(va, vb); if (elemDiff != NOP) { if (elemDiff instanceof Delta) { Delta delta = (Delta) elemDiff; Serializable deltaBase = delta.getBase(); if (!Objects.equals(va, deltaBase)) { // delta's base is not the old value // -> set a new value, don't use a delta update elemDiff = delta.getFullValue(); } // else delta's base is the in-database value // because base is consistent with old value, assume the delta is already properly computed } diff.put(key, elemDiff); } } } for (Entry<String, Serializable> en : b.entrySet()) { String key = en.getKey(); Serializable va = a.get(key); if (va != null) { // already checked by loop on a continue; } Serializable vb = en.getValue(); if (!equalsLoose(null, vb)) { // value must be added diff.put(key, vb); } } return diff; } /** * Changes the deltas stored into actual full values. * * @since 6.0 */ public static void resetDeltas(State state) { if (Boolean.parseBoolean(Framework.getProperty(DISABLED_DELTA_PROP, "false"))) { return; } for (Entry<String, Serializable> en : state.entrySet()) { Serializable value = en.getValue(); if (value instanceof State) { resetDeltas((State) value); } else if (value instanceof Delta) { state.put(en.getKey(), ((Delta) value).getFullValue()); } } } }