| /* |
| * 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 |
| } |
| } |