/* * Copyright (c) 2008-2016 Eike Stepper (Berlin, Germany) and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Simon McDuff - initial API and implementation * Eike Stepper - maintenance * Cyril Jaquier - Bug 310574 (with the help of Pascal Lehmann) */ package org.eclipse.emf.cdo.internal.common.revision.delta; import org.eclipse.emf.cdo.common.protocol.CDODataInput; import org.eclipse.emf.cdo.common.protocol.CDODataOutput; import org.eclipse.emf.cdo.common.revision.CDORevision; import org.eclipse.emf.cdo.common.revision.delta.CDOAddFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDOFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDOFeatureDeltaVisitor; import org.eclipse.emf.cdo.common.revision.delta.CDOListFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDOMoveFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDORemoveFeatureDelta; import org.eclipse.emf.cdo.common.revision.delta.CDOSetFeatureDelta; import org.eclipse.emf.cdo.spi.common.revision.CDOReferenceAdjuster; import org.eclipse.emf.cdo.spi.common.revision.InternalCDOFeatureDelta; import org.eclipse.net4j.util.ObjectUtil; import org.eclipse.net4j.util.collection.Pair; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.FeatureMapUtil; import java.io.IOException; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * @author Simon McDuff */ public class CDOListFeatureDeltaImpl extends CDOFeatureDeltaImpl implements CDOListFeatureDelta { private final int originSize; private final List<CDOFeatureDelta> listChanges = new ArrayList<CDOFeatureDelta>(); private transient int[] cachedIndices; private transient ListTargetAdding[] cachedSources; private transient List<CDOFeatureDelta> unprocessedFeatureDeltas; public CDOListFeatureDeltaImpl(EStructuralFeature feature, int originSize) { super(feature); this.originSize = originSize; } public CDOListFeatureDeltaImpl(CDODataInput in, EClass eClass) throws IOException { super(in, eClass); originSize = in.readInt(); int size = in.readInt(); for (int i = 0; i < size; i++) { listChanges.add(in.readCDOFeatureDelta(eClass)); } } public CDOListFeatureDelta copy() { CDOListFeatureDeltaImpl result = new CDOListFeatureDeltaImpl(getFeature(), getOriginSize()); Map<CDOFeatureDelta, CDOFeatureDelta> map = null; if (cachedSources != null || unprocessedFeatureDeltas != null) { map = new HashMap<CDOFeatureDelta, CDOFeatureDelta>(); } for (CDOFeatureDelta delta : listChanges) { CDOFeatureDelta newDelta = delta.copy(); result.listChanges.add(newDelta); if (map != null) { map.put(delta, newDelta); } } if (cachedIndices != null) { result.cachedIndices = copyOf(cachedIndices, cachedIndices.length); } if (cachedSources != null) { int length = cachedSources.length; result.cachedSources = new ListTargetAdding[length]; for (int i = 0; i < length; i++) { ListTargetAdding oldElement = cachedSources[i]; CDOFeatureDelta newElement = map.get(oldElement); if (newElement instanceof ListTargetAdding) { result.cachedSources[i] = (ListTargetAdding)newElement; } } } if (unprocessedFeatureDeltas != null) { int size = unprocessedFeatureDeltas.size(); result.unprocessedFeatureDeltas = new ArrayList<CDOFeatureDelta>(size); for (CDOFeatureDelta oldDelta : unprocessedFeatureDeltas) { CDOFeatureDelta newDelta = map.get(oldDelta); if (newDelta != null) { result.unprocessedFeatureDeltas.add(newDelta); } } } return result; } @Override public void write(CDODataOutput out, EClass eClass) throws IOException { super.write(out, eClass); out.writeInt(originSize); out.writeInt(listChanges.size()); for (CDOFeatureDelta featureDelta : listChanges) { out.writeCDOFeatureDelta(eClass, featureDelta); } } public Type getType() { return Type.LIST; } public int getOriginSize() { return originSize; } public List<CDOFeatureDelta> getListChanges() { return listChanges; } /** * Returns the number of indices as the first element of the array. * * @return never <code>null</code>. */ public Pair<ListTargetAdding[], int[]> reconstructAddedIndices() { reconstructAddedIndicesWithNoCopy(); return Pair.create(copyOf(cachedSources, cachedSources.length, cachedSources.getClass()), copyOf(cachedIndices, cachedIndices.length)); } private void reconstructAddedIndicesWithNoCopy() { // Note that cachedIndices and cachedSources are always either both null or // both non-null, and in the latter case, are always of the same length. // Furthermore, there can only be unprocessedFeatureDeltas if cachesIndices // and cachedSources are non-null. if (cachedIndices == null || unprocessedFeatureDeltas != null) { if (cachedIndices == null) { int initialCapacity = listChanges.size() + 1; cachedIndices = new int[initialCapacity]; cachedSources = new ListTargetAdding[initialCapacity]; } else // i.e. unprocessedFeatureDeltas != null { int requiredCapacity = 1 + cachedIndices[0] + unprocessedFeatureDeltas.size(); if (cachedIndices.length < requiredCapacity) { int newCapacity = Math.max(requiredCapacity, cachedIndices.length * 2); int[] newIndices = new int[newCapacity]; System.arraycopy(cachedIndices, 0, newIndices, 0, cachedIndices.length); cachedIndices = newIndices; ListTargetAdding[] newSources = new ListTargetAdding[newCapacity]; System.arraycopy(cachedSources, 0, newSources, 0, cachedSources.length); cachedSources = newSources; } } List<CDOFeatureDelta> featureDeltasToBeProcessed = unprocessedFeatureDeltas == null ? listChanges : unprocessedFeatureDeltas; for (CDOFeatureDelta featureDelta : featureDeltasToBeProcessed) { if (featureDelta instanceof ListIndexAffecting) { ListIndexAffecting affecting = (ListIndexAffecting)featureDelta; affecting.affectIndices(cachedSources, cachedIndices); } if (featureDelta instanceof ListTargetAdding) { cachedIndices[++cachedIndices[0]] = ((ListTargetAdding)featureDelta).getIndex(); cachedSources[cachedIndices[0]] = (ListTargetAdding)featureDelta; } } unprocessedFeatureDeltas = null; } } private boolean cleanupWithNewDelta(CDOFeatureDelta featureDelta) { EStructuralFeature feature = getFeature(); switch (featureDelta.getType()) { case REMOVE: { if (feature instanceof EReference || FeatureMapUtil.isFeatureMap(feature)) { Boolean result = cleanupWithNewRemoveDelta((CDORemoveFeatureDelta)featureDelta); if (result != null) { return result; } } break; } case SET: { Boolean result = cleanupWithNewSetDelta((CDOSetFeatureDelta)featureDelta); if (result != null) { return result; } break; } } if (cachedIndices != null) { if (unprocessedFeatureDeltas == null) { unprocessedFeatureDeltas = new ArrayList<CDOFeatureDelta>(); } unprocessedFeatureDeltas.add(featureDelta); } return true; } private Boolean cleanupWithNewRemoveDelta(CDORemoveFeatureDelta removeFeatureDelta) { int indexToRemove = removeFeatureDelta.getIndex(); reconstructAddedIndicesWithNoCopy(); for (int i = 1; i <= cachedIndices[0]; i++) { int index = cachedIndices[i]; if (indexToRemove == index) { // The previous implementation set the value of the feature delta to CDOID.NULL. Databinding and probably // others don't really like it. We now remove the ADD (or SET which seems to appear in CDOListFeatureDelta // during opposite adjustment!? Why???) and patch the other feature deltas. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=310574 ListTargetAdding delta = cachedSources[i]; // We use a "floating" index which is the index (in the list) of the item to remove at the time when the // object was still in the list. This index evolves with the feature deltas. int floatingIndex = delta.getIndex(); // First updates cachedSources and cachedIndices using CDORemoveFeatureDelta. ListIndexAffecting affecting = (ListIndexAffecting)removeFeatureDelta; affecting.affectIndices(cachedSources, cachedIndices); // Then adjusts the remaining feature deltas. boolean skip = true; ListIterator<CDOFeatureDelta> iterator = listChanges.listIterator(); while (iterator.hasNext()) { CDOFeatureDelta fd = iterator.next(); // We only need to process feature deltas that come after the ADD (or SET) to be removed. if (skip) { if (fd == delta) { // Found the ADD (or SET) feature delta that we need to remove. So remove it from the list and start // processing the next feature deltas. skip = false; iterator.remove(); // SET if (fd instanceof CDOSetFeatureDelta) { // If the removed delta is SET we add the REMOVE to the feature deltas. We do not need to adjust the // other feature deltas because SET do not modify the list. return true; } } continue; } // ADD if (fd instanceof CDOAddFeatureDelta) { // Increases the floating index if the ADD came in front of the item. if (((CDOAddFeatureDelta)fd).getIndex() <= floatingIndex) { ++floatingIndex; } // Adjusts the feature delta too. ((WithIndex)fd).adjustAfterRemoval(floatingIndex); } // REMOVE else if (fd instanceof CDORemoveFeatureDelta) { int idx = floatingIndex; // Decreases the floating index if the REMOVE came in front of the item. if (((CDORemoveFeatureDelta)fd).getIndex() <= floatingIndex) { --floatingIndex; } // Adjusts the feature delta too. ((WithIndex)fd).adjustAfterRemoval(idx); } // MOVE else if (fd instanceof CDOMoveFeatureDelta) { // Remembers the positions before we patch them. int from = ((CDOMoveFeatureDelta)fd).getOldPosition(); int to = ((CDOMoveFeatureDelta)fd).getNewPosition(); if (floatingIndex == from) { // We are moving the "to be deleted" item. So we update our floating index and remove the MOVE. It has // no effect on the list. floatingIndex = to; iterator.remove(); } else { // In the other cases, we need to patch the positions. // If the old position is greater or equal to the current position of the item to be removed (remember, // that's our floating index), decrease the position. int patchedFrom = floatingIndex <= from ? from - 1 : from; // The new position requires more care. We need to know the direction of the move (left-to-right or // right-to-left). int patchedTo; if (from > to) { // left-to-right. Only decreases the position if it is strictly greater than the current item // position. patchedTo = floatingIndex < to ? to - 1 : to; } else { // right-to-left. Decreases the position if it is greater or equal than the current item position. patchedTo = floatingIndex <= to ? to - 1 : to; } // We can now update our floating index. We use the original positions because the floating index // represents the item "to be deleted" before it was actually removed. if (from < floatingIndex && floatingIndex <= to) { --floatingIndex; } else if (to <= floatingIndex && floatingIndex < from) { ++floatingIndex; } // And finally adjust the feature delta. if (patchedFrom == patchedTo) { // Source and destination are the same so just remove the feature delta. iterator.remove(); } else { ((CDOMoveFeatureDeltaImpl)fd).setOldPosition(patchedFrom); ((CDOMoveFeatureDeltaImpl)fd).setNewPosition(patchedTo); } } } // SET else if (fd instanceof CDOSetFeatureDelta) { // Adjusts the feature delta too. ((WithIndex)fd).adjustAfterRemoval(floatingIndex); } } // If the removed delta was ADD so we do not add the REMOVE to the feature deltas. return false; } } return null; } /** * A new SET feature delta can interfere with former ADD or SET deltas. */ private Boolean cleanupWithNewSetDelta(CDOSetFeatureDelta featureDelta) { final class DeltaProxy implements InternalCDOFeatureDelta.WithIndex { private final int indexIntoListChanges; private int index; public DeltaProxy(int indexIntoListChanges, int index) { this.indexIntoListChanges = indexIntoListChanges; this.index = index; } public int getIndexIntoListChanges() { return indexIntoListChanges; } public int getIndex() { return index; } public void adjustAfterAddition(int index) { if (index <= this.index) { ++this.index; } } public void adjustAfterRemoval(int index) { if (index < this.index && this.index > 0) { --this.index; } } } int size = listChanges.size(); List<DeltaProxy> proxies = new ArrayList<DeltaProxy>(); for (int i = 0; i < size; i++) { CDOFeatureDelta fd = listChanges.get(i); switch (fd.getType()) { case MOVE: { int oldPosition = ((CDOMoveFeatureDelta)fd).getOldPosition(); int newPosition = ((CDOMoveFeatureDelta)fd).getNewPosition(); for (DeltaProxy proxy : proxies) { proxy.adjustAfterRemoval(oldPosition); proxy.adjustAfterAddition(newPosition); } break; } case REMOVE: { int index = ((CDORemoveFeatureDelta)fd).getIndex(); for (DeltaProxy proxy : proxies) { proxy.adjustAfterRemoval(index); } break; } case ADD: { int index = ((CDOAddFeatureDelta)fd).getIndex(); for (DeltaProxy proxy : proxies) { proxy.adjustAfterAddition(index); } proxies.add(new DeltaProxy(i, index)); break; } case SET: { int index = ((CDOSetFeatureDelta)fd).getIndex(); proxies.add(new DeltaProxy(i, index)); break; } } } int index = featureDelta.getIndex(); for (DeltaProxy proxy : proxies) { if (proxy.getIndex() == index) { int indexIntoListChanges = proxy.getIndexIntoListChanges(); CDOFeatureDelta featureDeltaToModify = listChanges.get(indexIntoListChanges); if (featureDeltaToModify.getType() == CDOFeatureDelta.Type.ADD) { ((CDOAddFeatureDeltaImpl)featureDeltaToModify).setValue(featureDelta.getValue()); return false; } // Here featureDeltaToModify is a SET delta listChanges.remove(indexIntoListChanges); // Replace the SET delta that existed for this index break; } } return null; } public void add(CDOFeatureDelta featureDelta) { // Only adds the feature delta to the list if required. if (cleanupWithNewDelta(featureDelta)) { listChanges.add(featureDelta); } } public Object applyTo(CDORevision revision) { for (CDOFeatureDelta featureDelta : listChanges) { ((CDOFeatureDeltaImpl)featureDelta).applyTo(revision); } return null; } @Override public boolean adjustReferences(CDOReferenceAdjuster adjuster) { boolean changed = false; for (CDOFeatureDelta featureDelta : listChanges) { changed |= ((CDOFeatureDeltaImpl)featureDelta).adjustReferences(adjuster); } return changed; } public void accept(CDOFeatureDeltaVisitor visitor) { visitor.visit(this); } @Override public boolean isStructurallyEqual(Object obj) { if (!super.isStructurallyEqual(obj)) { return false; } CDOListFeatureDelta that = (CDOListFeatureDelta)obj; return ObjectUtil.equals(listChanges, that.getListChanges()); } @Override protected String toStringAdditional() { return "originSize=" + originSize + ", list=" + listChanges; //$NON-NLS-1$ } /** * Copied from JAVA 1.6 {@link Arrays Arrays.copyOf}. */ private static <T, U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = (Object)newType == (Object)Object[].class ? (T[])new Object[newLength] : (T[])Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } /** * Copied from JAVA 1.6 {@link Arrays Arrays.copyOf}. */ private static int[] copyOf(int[] original, int newLength) { int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } }