| /******************************************************************************* |
| * 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(); |
| } |
| |
| } |