blob: 7934b97b23392ca4e325df22670dd1a4efb59f40 [file] [log] [blame]
/*
* Copyright (c) 2011-2013, 2015 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:
* Caspar De Groot - initial API and implementation
*/
package org.eclipse.emf.internal.cdo.util;
import org.eclipse.emf.cdo.CDOObject;
import org.eclipse.emf.cdo.CDOState;
import org.eclipse.emf.cdo.common.id.CDOID;
import org.eclipse.emf.cdo.common.id.CDOIDUtil;
import org.eclipse.emf.cdo.common.revision.delta.CDOAddFeatureDelta;
import org.eclipse.emf.cdo.common.revision.delta.CDOClearFeatureDelta;
import org.eclipse.emf.cdo.common.revision.delta.CDOContainerFeatureDelta;
import org.eclipse.emf.cdo.common.revision.delta.CDOFeatureDelta;
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.CDORevisionDelta;
import org.eclipse.emf.cdo.common.revision.delta.CDOSetFeatureDelta;
import org.eclipse.emf.cdo.common.revision.delta.CDOUnsetFeatureDelta;
import org.eclipse.emf.cdo.eresource.CDOResource;
import org.eclipse.emf.cdo.spi.common.model.InternalCDOClassInfo;
import org.eclipse.emf.cdo.spi.common.revision.InternalCDORevision;
import org.eclipse.emf.cdo.util.CDOUtil;
import org.eclipse.emf.cdo.util.CommitIntegrityException;
import org.eclipse.net4j.util.CheckUtil;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.spi.cdo.InternalCDOObject;
import org.eclipse.emf.spi.cdo.InternalCDOTransaction;
import org.eclipse.emf.spi.cdo.InternalCDOTransaction.InternalCDOCommitContext;
import java.util.HashSet;
import java.util.Set;
/**
* @author Caspar De Groot
* @since 4.0
*/
public class CommitIntegrityCheck
{
private InternalCDOTransaction transaction;
private Style style;
private Set<CDOID> newIDs, dirtyIDs, detachedIDs;
private Set<CDOObject> missingObjects = new HashSet<CDOObject>();
private StringBuilder exceptionMessage = new StringBuilder();
public CommitIntegrityCheck(InternalCDOCommitContext commitContext)
{
this(commitContext, Style.EXCEPTION_FAST);
}
public CommitIntegrityCheck(InternalCDOCommitContext commitContext, Style style)
{
transaction = commitContext.getTransaction();
CheckUtil.checkArg(style, "style");
this.style = style;
newIDs = commitContext.getNewObjects().keySet();
dirtyIDs = commitContext.getDirtyObjects().keySet();
detachedIDs = commitContext.getDetachedObjects().keySet();
}
public void check() throws CommitIntegrityException
{
// For new objects: ensure that their container is included,
// as well as the targets of the new object's bidi references
for (CDOID newID : newIDs)
{
CDOObject newObject = transaction.getObject(newID);
checkContainerIncluded(newObject, "new");
checkCurrentRefTargetsIncluded(newObject, "new");
}
// For detached objects: ensure that their former container is included,
// as well as the targets of the detached object's bidi references
for (CDOID detachedID : detachedIDs)
{
CDOObject detachedObject = transaction.getObject(detachedID);
checkFormerContainerIncluded(detachedObject);
checkFormerBidiRefTargetsIncluded(detachedObject, "detached");
}
// For dirty objects: if any of the deltas for the object, affect containment (i.e. object was moved)
// or a bi-di reference, ensure that for containment, both the old and new containers are included,
// (or that the child is included if we are considering the dirty parent),
// and that for a bi-di reference, the object holding the other end of the bi-di is included,
// as well as possibly the *former* object holding the other end.
for (CDOID dirtyID : dirtyIDs)
{
CDOObject dirtyObject = transaction.getObject(dirtyID);
analyzeRevisionDelta((InternalCDOObject)dirtyObject);
}
if (!missingObjects.isEmpty() && style == Style.EXCEPTION)
{
throw createException();
}
}
public Set<? extends EObject> getMissingObjects()
{
return missingObjects;
}
private CDOID getContainerOrResourceID(InternalCDORevision revision)
{
CDOID containerOrResourceID = null;
Object idOrObject = revision.getContainerID();
if (idOrObject != null)
{
containerOrResourceID = (CDOID)transaction.convertObjectToID(idOrObject);
}
if (CDOIDUtil.isNull(containerOrResourceID))
{
containerOrResourceID = revision.getResourceID();
}
return containerOrResourceID;
}
private void analyzeRevisionDelta(InternalCDOObject dirtyObject) throws CommitIntegrityException
{
// Getting the deltas from the TX is not a good idea...
// We better recompute a fresh delta:
InternalCDORevision cleanRev = transaction.getCleanRevisions().get(dirtyObject);
CheckUtil.checkNull(cleanRev, "Could not obtain clean revision for dirty object " + dirtyObject);
InternalCDOClassInfo classInfo = dirtyObject.cdoClassInfo();
InternalCDORevision dirtyRev = dirtyObject.cdoRevision();
CDORevisionDelta revisionDelta = dirtyRev.compare(cleanRev);
for (CDOFeatureDelta featureDelta : revisionDelta.getFeatureDeltas())
{
EStructuralFeature feature = featureDelta.getFeature();
if (feature == CDOContainerFeatureDelta.CONTAINER_FEATURE)
{
// Three possibilities here:
// 1. Object's container has changed
// 2. Object's containment feature has changed
// 3. Object's resource has changed
// (or several of the above)
// @1
CDOID currentContainerID = (CDOID)transaction.convertObjectToID(dirtyRev.getContainerID());
CDOID cleanContainerID = (CDOID)transaction.convertObjectToID(cleanRev.getContainerID());
if (!CDOIDUtil.equals(currentContainerID, cleanContainerID))
{
if (currentContainerID != CDOID.NULL)
{
checkIncluded(currentContainerID, "container of moved", dirtyObject);
}
if (cleanContainerID != CDOID.NULL)
{
checkIncluded(cleanContainerID, "former container of moved", dirtyObject);
}
}
// @2
// Nothing to be done. (I think...)
// @3
CDOID currentResourceID = dirtyRev.getResourceID();
CDOID cleanResourceID = cleanRev.getResourceID();
if (!CDOIDUtil.equals(currentResourceID, cleanResourceID))
{
if (currentResourceID != CDOID.NULL)
{
checkIncluded(currentResourceID, "resource of moved", dirtyObject);
}
if (cleanResourceID != CDOID.NULL)
{
checkIncluded(cleanResourceID, "former resource of moved", dirtyObject);
}
}
}
else if (feature instanceof EReference)
{
if (featureDelta instanceof CDOListFeatureDelta)
{
boolean hasPersistentOpposite = classInfo.hasPersistentOpposite(feature);
for (CDOFeatureDelta innerFeatDelta : ((CDOListFeatureDelta)featureDelta).getListChanges())
{
checkFeatureDelta(innerFeatDelta, hasPersistentOpposite, dirtyObject);
}
}
else
{
boolean hasPersistentOpposite = classInfo.hasPersistentOpposite(feature);
checkFeatureDelta(featureDelta, hasPersistentOpposite, dirtyObject);
}
}
}
}
private void checkIncluded(Object idOrObject, String msg, CDOObject o) throws CommitIntegrityException
{
idOrObject = transaction.convertObjectToID(idOrObject);
if (idOrObject instanceof CDOID)
{
CDOID id = (CDOID)idOrObject;
if (!id.isNull())
{
checkIncluded(id, msg, o);
}
}
// else: Transient object -- ignore
}
private void checkFeatureDelta(CDOFeatureDelta featureDelta, boolean hasPersistentOpposite, CDOObject dirtyObject)
throws CommitIntegrityException
{
EReference ref = (EReference)featureDelta.getFeature();
boolean containmentOrWithOpposite = ref.isContainment() || hasPersistentOpposite;
if (featureDelta instanceof CDOAddFeatureDelta)
{
Object idOrObject = ((CDOAddFeatureDelta)featureDelta).getValue();
if (containmentOrWithOpposite || isNew(idOrObject))
{
checkIncluded(idOrObject, "added child / refTarget of", dirtyObject);
}
}
else if (featureDelta instanceof CDOSetFeatureDelta)
{
Object oldIDOrObject = ((CDOSetFeatureDelta)featureDelta).getOldValue();
CDOID oldID = (CDOID)transaction.convertObjectToID(oldIDOrObject);
if (!CDOIDUtil.isNull(oldID))
{
// Old child must be included if it's the container or has an eOpposite
if (containmentOrWithOpposite)
{
checkIncluded(oldID, "removed / former child / refTarget of", dirtyObject);
}
}
Object newIDOrObject = ((CDOSetFeatureDelta)featureDelta).getValue();
if (newIDOrObject != null)
{
// New child must be included
newIDOrObject = transaction.convertObjectToID(newIDOrObject);
if (containmentOrWithOpposite || isNew(newIDOrObject))
{
checkIncluded(newIDOrObject, "new child / refTarget of", dirtyObject);
}
}
}
else if (containmentOrWithOpposite)
{
if (featureDelta instanceof CDORemoveFeatureDelta)
{
Object idOrObject = ((CDORemoveFeatureDelta)featureDelta).getValue();
CDOID id = (CDOID)transaction.convertObjectToID(idOrObject);
checkIncluded(id, "removed child / refTarget of", dirtyObject);
}
else if (featureDelta instanceof CDOClearFeatureDelta)
{
EStructuralFeature feat = ((CDOClearFeatureDelta)featureDelta).getFeature();
InternalCDORevision cleanRev = transaction.getCleanRevisions().get(dirtyObject);
int n = cleanRev.size(feat);
for (int i = 0; i < n; i++)
{
Object idOrObject = cleanRev.get(feat, i);
CDOID id = (CDOID)transaction.convertObjectToID(idOrObject);
checkIncluded(id, "removed child / refTarget of", dirtyObject);
}
}
else if (featureDelta instanceof CDOUnsetFeatureDelta)
{
EStructuralFeature feat = ((CDOUnsetFeatureDelta)featureDelta).getFeature();
InternalCDORevision cleanRev = transaction.getCleanRevisions().get(dirtyObject);
Object idOrObject = cleanRev.getValue(feat);
CDOID id = (CDOID)transaction.convertObjectToID(idOrObject);
checkIncluded(id, "removed child / refTarget of", dirtyObject);
}
else if (featureDelta instanceof CDOMoveFeatureDelta)
{
// Nothing to do: a move doesn't affect the child being moved
// so that child does not need to be included
}
else
{
throw new IllegalArgumentException("Unexpected delta type: " + featureDelta.getClass().getSimpleName());
}
}
}
private boolean isNew(Object idOrObject)
{
CDOObject object = null;
if (idOrObject instanceof CDOObject)
{
object = (CDOObject)idOrObject;
}
else if (idOrObject instanceof EObject)
{
object = CDOUtil.getCDOObject((EObject)idOrObject);
}
else if (idOrObject instanceof CDOID)
{
object = transaction.getObject((CDOID)idOrObject);
}
if (object != null)
{
return object.cdoState() == CDOState.NEW;
}
return false;
}
private void checkIncluded(CDOID id, String msg, CDOObject o) throws CommitIntegrityException
{
if (id.isNull())
{
throw new IllegalArgumentException("CDOID must not be NULL");
}
if (!dirtyIDs.contains(id) && !detachedIDs.contains(id) && !newIDs.contains(id))
{
CDOObject missingObject = transaction.getObject(id);
if (missingObject == null)
{
throw new IllegalStateException("Could not find object for CDOID " + id);
}
missingObjects.add(missingObject);
if (exceptionMessage.length() > 0)
{
exceptionMessage.append('\n');
}
String m = String.format("The %s object %s needs to be included in the commit but isn't", msg, o);
exceptionMessage.append(m);
if (style == Style.EXCEPTION_FAST)
{
throw createException();
}
}
}
private CommitIntegrityException createException()
{
return new CommitIntegrityException(exceptionMessage.toString(), missingObjects);
}
/**
* Checks whether the container of a given object is included in the commit
*/
private void checkContainerIncluded(CDOObject object, String msgFrag) throws CommitIntegrityException
{
EObject eContainer = object.eContainer();
if (eContainer == null)
{
// It's a top-level object
CDOResource resource = object.cdoDirectResource();
checkIncluded(resource.cdoID(), "resource of " + msgFrag, object);
}
else
{
CDOObject container = CDOUtil.getCDOObject(eContainer);
checkIncluded(container.cdoID(), "container of " + msgFrag, object);
}
}
private void checkCurrentRefTargetsIncluded(CDOObject referencer, String msgFrag) throws CommitIntegrityException
{
InternalCDOClassInfo classInfo = ((InternalCDOObject)referencer).cdoClassInfo();
for (EReference reference : classInfo.getAllPersistentReferences())
{
if (reference.isMany())
{
EList<?> list = (EList<?>)referencer.eGet(reference);
if (!list.isEmpty())
{
boolean hasPersistentOpposite = classInfo.hasPersistentOpposite(reference);
for (Object refTarget : list)
{
checkBidiRefTargetOrNewNonBidiTargetIncluded(referencer, reference, refTarget, hasPersistentOpposite,
msgFrag);
}
}
}
else
{
Object refTarget = referencer.eGet(reference);
if (refTarget != null)
{
boolean hasPersistentOpposite = classInfo.hasPersistentOpposite(reference);
checkBidiRefTargetOrNewNonBidiTargetIncluded(referencer, reference, refTarget, hasPersistentOpposite,
msgFrag);
}
}
}
}
private void checkBidiRefTargetOrNewNonBidiTargetIncluded(CDOObject referencer, EReference eRef, Object refTarget,
boolean hasPersistentOpposite, String msgFrag) throws CommitIntegrityException
{
if (hasPersistentOpposite)
{
// It's a bi-di ref; the target must definitely be included
checkBidiRefTargetIncluded(refTarget, referencer, eRef.getName(), msgFrag);
}
else if (isNew(refTarget))
{
// It's a non-bidi ref; the target doesn't have to be included unless it's NEW
checkIncluded(refTarget, "target of reference '" + eRef.getName() + "' of " + msgFrag, referencer);
}
}
private void checkFormerBidiRefTargetsIncluded(CDOObject referencer, String msgFrag) throws CommitIntegrityException
{
// The referencer argument should really be a detached object, and so we know
// that we can find the pre-detach revision in tx.getFormerRevisions(). However,
// the object may have already been dirty prior to detachment, so we check the
// clean revisions first.
InternalCDORevision cleanRev = transaction.getCleanRevisions().get(referencer);
CheckUtil.checkState(cleanRev, "cleanRev");
InternalCDOClassInfo referencerClassInfo = ((InternalCDOObject)referencer).cdoClassInfo();
for (EReference reference : referencerClassInfo.getAllPersistentReferences())
{
if (referencerClassInfo.hasPersistentOpposite(reference))
{
if (reference.isMany())
{
EList<?> list = cleanRev.getList(reference);
if (list != null)
{
for (Object element : list)
{
checkBidiRefTargetIncluded(element, referencer, reference.getName(), msgFrag);
}
}
}
else
{
Object value = cleanRev.getValue(reference);
if (value != null)
{
checkBidiRefTargetIncluded(value, referencer, reference.getName(), msgFrag);
}
}
}
}
}
private void checkBidiRefTargetIncluded(Object refTarget, CDOObject referencer, String refName, String msgFrag)
throws CommitIntegrityException
{
CheckUtil.checkArg(refTarget, "refTarget");
CDOID refTargetID = null;
if (refTarget instanceof EObject)
{
refTargetID = CDOUtil.getCDOObject((EObject)refTarget).cdoID();
if (refTargetID == null)
{
// No ID, means object is TRANSIENT; ignore.
return;
}
}
else if (refTarget instanceof CDOID)
{
refTargetID = (CDOID)refTarget;
}
checkIncluded(refTargetID, "target of reference '" + refName + "' of " + msgFrag, referencer);
}
private void checkFormerContainerIncluded(CDOObject detachedObject) throws CommitIntegrityException
{
InternalCDORevision rev = transaction.getCleanRevisions().get(detachedObject);
CheckUtil.checkNull(rev, "Could not obtain clean revision for detached object " + detachedObject);
CDOID id = getContainerOrResourceID(rev);
checkIncluded(id, "former container (or resource) of detached", detachedObject);
}
/**
* Designates an exception style for a {@link CommitIntegrityCheck}
*
* @author Caspar De Groot
*/
public static enum Style
{
/**
* Throw an exception as soon as this {@link CommitIntegrityCheck} encounters the first problem
*/
EXCEPTION_FAST,
/**
* Throw an exception when this {@link CommitIntegrityCheck} finishes performing all possible checks, in case any
* problems were found
*/
EXCEPTION,
/**
* Do not throw an exception. Caller must invoke {@link CommitIntegrityCheck#getMissingObjects()} to find out if the
* check discovered any problems.
*/
NO_EXCEPTION
}
}