blob: 3cfd409204502f7f1460825de0aa9723cec825ac [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012-2013 EclipseSource Muenchen GmbH 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:
* Maximilian Koegel - initial API and implementation
* Edgar Mueller - custom reservation set modifiers
* Edgar Mueller - Bug 418183: recognizing conflicts on map entries
******************************************************************************/
package org.eclipse.emf.emfstore.internal.server.conflictDetection;
import static org.eclipse.emf.emfstore.internal.server.model.versioning.operations.util.OperationUtil.isMultiMoveRef;
import static org.eclipse.emf.emfstore.internal.server.model.versioning.operations.util.OperationUtil.isMultiRef;
import static org.eclipse.emf.emfstore.internal.server.model.versioning.operations.util.OperationUtil.isMultiRefSet;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.emfstore.internal.common.ExtensionRegistry;
import org.eclipse.emf.emfstore.internal.common.model.ModelElementId;
import org.eclipse.emf.emfstore.internal.common.model.ModelElementIdToEObjectMapping;
import org.eclipse.emf.emfstore.internal.common.model.util.ModelUtil;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.AbstractOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.CompositeOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.ContainmentType;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.CreateDeleteOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.FeatureOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.ReferenceOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.SingleReferenceOperation;
/**
* Map from model elements and their features to conflict candidate buckets.
*
* @author mkoegel
* @author emueller
*/
public class ReservationToConflictBucketCandidateMap {
private static final String KEY = "key"; //$NON-NLS-1$
private static ReservationSetModifier reservationSetModifier = initCustomReservationSetModifier();
private final ReservationSet reservationToConflictMap;
private final Set<ConflictBucketCandidate> conflictBucketCandidates;
private final Map<String, Integer> idToKeyHashCode;
private static ReservationSetModifier initCustomReservationSetModifier() {
return ExtensionRegistry.INSTANCE.get(
ReservationSetModifier.ID,
ReservationSetModifier.class,
new ReservationSetModifier() {
public ReservationSet addCustomReservation(
AbstractOperation operation, ReservationSet reservationSet,
ModelElementIdToEObjectMapping mapping) {
return reservationSet;
}
},
true);
}
/**
* Default constructor.
*/
public ReservationToConflictBucketCandidateMap() {
reservationToConflictMap = new ReservationSet();
conflictBucketCandidates = new LinkedHashSet<ConflictBucketCandidate>();
idToKeyHashCode = new LinkedHashMap<String, Integer>();
}
private void joinReservationSet(ReservationSet reservationSet,
ConflictBucketCandidate currentConflictBucketCandidate) {
final Set<String> modelElements = reservationSet.getAllModelElements();
// iterate incoming reservation set
for (final String modelElement : modelElements) {
// handle full reservations
if (reservationSet.hasFullReservation(modelElement)
|| reservationToConflictMap.hasFullReservation(modelElement)) {
final ConflictBucketCandidate mergedConflictBucketCandidates = mergeConflictBucketCandidates(
reservationToConflictMap
.getConflictBucketCandidates(modelElement),
currentConflictBucketCandidate);
reservationToConflictMap.addFullReservation(modelElement, mergedConflictBucketCandidates);
continue;
}
// at this point: neither full reservation of model element is existing nor requested to be joined
// handle existence reservations
if (reservationSet.hasExistenceReservation(modelElement)) {
reservationToConflictMap.addExistenceReservation(modelElement, currentConflictBucketCandidate);
}
// handle feature reservations
final Set<String> featureNames = reservationSet.getFeatureNames(modelElement);
for (final String featureName : featureNames) {
if (featureName.equals(FeatureNameReservationMap.EXISTENCE_FEATURE)) {
continue;
}
// we do not care about existence reservations since they will not collide with any other features
// handle normal feature reservations without opposite
if (!reservationSet.hasOppositeReservations(modelElement, featureName)) {
handleFeatureVsFeatureReservation(currentConflictBucketCandidate, modelElement, featureName);
} else {
handleOppositeVsOppositeReservation(reservationSet, currentConflictBucketCandidate, modelElement,
featureName);
}
}
}
}
private void handleFeatureVsFeatureReservation(ConflictBucketCandidate currentConflictBucketCandidate,
String modelElement, String featureName) {
if (reservationToConflictMap.hasFeatureReservation(modelElement, featureName)) {
final Set<ConflictBucketCandidate> existingBuckets = reservationToConflictMap
.getConflictBucketCandidates(
modelElement, featureName);
final ConflictBucketCandidate mergedConflictBucketCandidates = mergeConflictBucketCandidates(
existingBuckets,
currentConflictBucketCandidate);
reservationToConflictMap.addFeatureReservation(modelElement, featureName, mergedConflictBucketCandidates);
} else if (reservationToConflictMap.hasOppositeReservations(modelElement, featureName)) {
throw new IllegalStateException(
Messages.ReservationToConflictBucketCandidateMap_Illegal_Reservation_With_And_Without_Opposite);
} else {
reservationToConflictMap.addFeatureReservation(modelElement, featureName, currentConflictBucketCandidate);
}
}
private void handleOppositeVsOppositeReservation(ReservationSet reservationSet,
ConflictBucketCandidate currentConflictBucketCandidate, String modelElement, String featureName) {
if (reservationToConflictMap.hasOppositeReservations(modelElement, featureName)) {
final Set<String> opposites = reservationSet.getOpposites(modelElement, featureName);
for (final String oppositeModelElement : opposites) {
if (reservationToConflictMap.hasOppositeReservation(modelElement, featureName, oppositeModelElement)) {
final ConflictBucketCandidate mergedConflictBucketCandidates = mergeConflictBucketCandidates(
reservationToConflictMap
.getConflictBucketCandidates(
modelElement,
featureName,
oppositeModelElement),
currentConflictBucketCandidate);
reservationToConflictMap.addMultiReferenceWithOppositeReservation(
modelElement,
featureName,
oppositeModelElement,
mergedConflictBucketCandidates);
}
}
} else if (reservationToConflictMap.hasFeatureReservation(modelElement, featureName)) {
throw new IllegalStateException(
Messages.ReservationToConflictBucketCandidateMap_Illegal_Reservation_With_And_Without_Opposite);
} else {
final Set<String> opposites = reservationSet.getOpposites(modelElement, featureName);
for (final String oppositeModelElement : opposites) {
reservationToConflictMap.addMultiReferenceWithOppositeReservation(
modelElement,
featureName,
oppositeModelElement,
currentConflictBucketCandidate);
}
}
}
private ConflictBucketCandidate mergeConflictBucketCandidates(Set<ConflictBucketCandidate> existingBuckets,
ConflictBucketCandidate currentBucket) {
final ConflictBucketCandidate rootBucket = currentBucket.getRootConflictBucketCandidate();
for (final ConflictBucketCandidate otherBucket : existingBuckets) {
otherBucket.getRootConflictBucketCandidate().setParentConflictBucketCandidate(rootBucket);
}
return rootBucket;
}
/**
* Scans the given {@link AbstractOperation} into the reservation set.
*
* @param operation
* the operation to be scanned
* @param priority
* the global priority of the operation, which is later used to represent a conflict bucket candidate
* @param idToEObjectMapping
* a mapping from IDs to model elements containing all involved model elements
* @param isMyOperation
* whether this operation is incoming, {@code false} if it is, {@code true} otherwise
*/
public void scanOperationReservations(AbstractOperation operation, int priority,
ModelElementIdToEObjectMapping idToEObjectMapping, boolean isMyOperation) {
ReservationSet reservationSet = extractReservationFromOperation(operation, new ReservationSet(),
idToEObjectMapping);
reservationSet = addCustomReservations(operation, reservationSet, idToEObjectMapping);
final ConflictBucketCandidate conflictBucketCandidate = new ConflictBucketCandidate();
conflictBucketCandidates.add(conflictBucketCandidate);
conflictBucketCandidate.addOperation(operation, isMyOperation, priority);
joinReservationSet(reservationSet, conflictBucketCandidate);
}
private ReservationSet addCustomReservations(AbstractOperation operation, ReservationSet reservationSet,
ModelElementIdToEObjectMapping idToEObjectMapping) {
return reservationSetModifier.addCustomReservation(operation, reservationSet, idToEObjectMapping);
}
private ReservationSet extractReservationFromOperation(final AbstractOperation operation,
ReservationSet reservationSet, ModelElementIdToEObjectMapping idToEObjectMapping) {
if (operation instanceof CompositeOperation) {
final CompositeOperation compositeOperation = (CompositeOperation) operation;
for (final AbstractOperation subOperation : compositeOperation.getSubOperations()) {
extractReservationFromOperation(subOperation, reservationSet, idToEObjectMapping);
}
return reservationSet;
} else if (operation instanceof CreateDeleteOperation) {
// add full reservations only for delete operations and their deleted elements
final CreateDeleteOperation createDeleteOperation = (CreateDeleteOperation) operation;
if (createDeleteOperation.isDelete()) {
// handle containment tree
for (final ModelElementId modelElementId : createDeleteOperation.getEObjectToIdMap().values()) {
reservationSet.addFullReservation(modelElementId.getId());
}
} else {
// check for map entries
for (final EObject eObject : createDeleteOperation.getEObjectToIdMap().keySet()) {
if (!isMapEntry(eObject)) {
continue;
}
handleMapEntry((Entry<?, ?>) eObject, createDeleteOperation, idToEObjectMapping);
}
}
// handle suboperations
for (final AbstractOperation subOperation : createDeleteOperation.getSubOperations()) {
extractReservationFromOperation(subOperation, reservationSet, idToEObjectMapping);
}
return reservationSet;
} else if (operation instanceof FeatureOperation) {
handleFeatureOperation(operation, reservationSet, idToEObjectMapping);
return reservationSet;
}
throw new IllegalStateException(Messages.ReservationToConflictBucketCandidateMap_Unknown_Operation
+ operation.getClass().getCanonicalName());
}
private boolean isMapEntry(EObject eObject) {
return eObject instanceof Map.Entry<?, ?>;
}
private void handleMapEntry(final Map.Entry<?, ?> mapEntry, CreateDeleteOperation createDeleteOperation,
ModelElementIdToEObjectMapping idToEObjectMapping) {
final String mapEntryId = createDeleteOperation.getEObjectToIdMap().get(mapEntry).getId();
if (mapEntry.getKey() != null) {
idToKeyHashCode.put(mapEntryId, new Integer(mapEntry.getKey().hashCode()));
return;
}
// if entry's key is null, fetch sub operation, check feature name for key, and resolve ID to get a
// string representation for the key
final ReferenceOperation keyReferenceOperation = getKeyReferenceOperation(createDeleteOperation);
if (keyReferenceOperation != null) {
final Set<ModelElementId> otherInvolvedModelElements = keyReferenceOperation
.getOtherInvolvedModelElements();
final Iterator<ModelElementId> iterator = otherInvolvedModelElements.iterator();
if (iterator.hasNext()) {
final ModelElementId otherId = iterator.next();
final EObject key = idToEObjectMapping.get(otherId);
if (key != null) {
idToKeyHashCode.put(mapEntryId, new Integer(key.hashCode()));
} else {
ModelUtil.logWarning(Messages.ReservationToConflictBucketCandidateMap_Key_Is_Null);
}
}
} else {
ModelUtil.logWarning(
MessageFormat.format(Messages.ReservationToConflictBucketCandidateMap_SingleRefOp_Of_CreateOp_Missing,
createDeleteOperation.getOperationId()));
}
}
/**
* Tries to find the operation that set's the key attribute of a map entry.
*
* @param createDeleteOperation
* @return
*/
private ReferenceOperation getKeyReferenceOperation(CreateDeleteOperation createDeleteOperation) {
for (final AbstractOperation op : createDeleteOperation.getSubOperations()) {
if (!SingleReferenceOperation.class.isInstance(op)) {
continue;
}
final SingleReferenceOperation singleReferenceOperation = SingleReferenceOperation.class.cast(op);
if (singleReferenceOperation.getFeatureName().equals(KEY)) {
return singleReferenceOperation;
}
}
return null;
}
private void handleFeatureOperation(AbstractOperation operation, ReservationSet reservationSet,
ModelElementIdToEObjectMapping idToEObjectMapping) {
final FeatureOperation featureOperation = (FeatureOperation) operation;
final String modelElementId = featureOperation.getModelElementId().getId();
final String featureName = featureOperation.getFeatureName();
if (featureOperation instanceof ReferenceOperation) {
final ReferenceOperation referenceOperation = (ReferenceOperation) featureOperation;
// if (isRemovingReferencesOnly(referenceOperation)) {
// return;
// }
for (final ModelElementId otherModelElement : referenceOperation.getOtherInvolvedModelElements()) {
if (referenceOperation.getContainmentType().equals(ContainmentType.CONTAINMENT)
&& !referenceOperation.isBidirectional()) {
reservationSet.addContainerReservation(otherModelElement.getId());
} else {
reservationSet.addExistenceReservation(otherModelElement.getId());
}
}
}
if (isMultiRef(featureOperation) || isMultiRefSet(featureOperation) || isMultiMoveRef(featureOperation)) {
for (final ModelElementId otherModelElement : featureOperation.getOtherInvolvedModelElements()) {
reservationSet.addMultiReferenceWithOppositeReservation(modelElementId, featureName,
otherModelElement.getId());
Integer hashCode = null;
if (isCreatedMapEntry(otherModelElement)) {
hashCode = getKeyHashCode(otherModelElement);
} else if (isMapEntry(idToEObjectMapping.get(otherModelElement))) {
hashCode = getKeyHashCode(idToEObjectMapping, otherModelElement);
}
if (hashCode != null) {
reservationSet.addMapKeyReservation(modelElementId, featureName,
hashCode.toString());
}
}
} else {
reservationSet.addFeatureReservation(modelElementId, featureName);
}
return;
}
private Integer getKeyHashCode(ModelElementIdToEObjectMapping idToEObjectMapping, ModelElementId keyId) {
final Map.Entry<?, ?> mapEntry = (Entry<?, ?>) idToEObjectMapping.get(keyId);
if (mapEntry.getKey() != null) {
return new Integer(mapEntry.getKey().hashCode());
}
return null;
}
private Integer getKeyHashCode(ModelElementId keyId) {
return idToKeyHashCode.get(keyId);
}
private boolean isCreatedMapEntry(ModelElementId modelElementId) {
return idToKeyHashCode.containsKey(modelElementId.getId());
}
/**
* Get all Conflict Buckets that are part of the map.
*
* @return a set of conflict bucket candidates
*/
public Set<ConflictBucketCandidate> getConflictBucketCandidates() {
final Map<ConflictBucketCandidate, Set<ConflictBucketCandidate>> rootToBucketMergeSetMap = new LinkedHashMap<ConflictBucketCandidate, Set<ConflictBucketCandidate>>();
for (final ConflictBucketCandidate candidate : conflictBucketCandidates) {
final ConflictBucketCandidate root = candidate.getRootConflictBucketCandidate();
Set<ConflictBucketCandidate> bucketMergeSet = rootToBucketMergeSetMap.get(root);
if (bucketMergeSet == null) {
bucketMergeSet = new LinkedHashSet<ConflictBucketCandidate>();
rootToBucketMergeSetMap.put(root, bucketMergeSet);
}
bucketMergeSet.add(candidate);
}
for (final ConflictBucketCandidate root : rootToBucketMergeSetMap.keySet()) {
for (final ConflictBucketCandidate sibling : rootToBucketMergeSetMap.get(root)) {
root.addConflictBucketCandidate(sibling);
}
}
return rootToBucketMergeSetMap.keySet();
}
}