blob: e141702aa380f5d8367e40a9fcfd80f51a20974a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2017 Obeo 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:
* Obeo - initial API and implementation
* Martin Fleck - Consider profile definition changes on origin (bug 495259)
* Philip Langer - bug 508665, progress reporting
* Christian W. Damus - bug 522064
*******************************************************************************/
package org.eclipse.emf.compare.uml2.internal.postprocessor;
import static org.eclipse.emf.compare.internal.utils.ComparisonUtil.delete;
import static org.eclipse.emf.compare.internal.utils.ComparisonUtil.isAddOrSetDiff;
import static org.eclipse.emf.compare.internal.utils.ComparisonUtil.isDeleteOrUnsetDiff;
import static org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareIterables.tryFirst;
import static org.eclipse.emf.compare.utils.MatchUtil.getOriginObject;
import static org.eclipse.uml2.uml.UMLPackage.Literals.INSTANCE_SPECIFICATION__CLASSIFIER;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.ComparisonCanceledException;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.ReferenceChange;
import org.eclipse.emf.compare.internal.postprocessor.factories.IChangeFactory;
import org.eclipse.emf.compare.postprocessor.IPostProcessor;
import org.eclipse.emf.compare.uml2.internal.UMLCompareMessages;
import org.eclipse.emf.compare.uml2.internal.UMLComparePlugin;
import org.eclipse.emf.compare.uml2.internal.UMLDiff;
import org.eclipse.emf.compare.uml2.internal.postprocessor.extension.UMLExtensionFactoryRegistry;
import org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareUtil;
import org.eclipse.emf.compare.utils.ReferenceUtil;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.URIConverter;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.uml2.uml.Enumeration;
import org.eclipse.uml2.uml.EnumerationLiteral;
import org.eclipse.uml2.uml.ProfileApplication;
/**
* Post-processor to create the UML difference extensions.
*
* @author <a href="mailto:cedric.notot@obeo.fr">Cedric Notot</a>
*/
public class UMLPostProcessor implements IPostProcessor {
/**
* Predicate to find the match of an annotation referencing a profile definition, within a list a matches.
*/
private static final Predicate<Match> ANNOTATION_REFERENCING_PROFILE_DEFINITION = new Predicate<Match>() {
private Pattern umlNsPattern = Pattern.compile("http://www\\.eclipse\\.org/uml2/.*/UML"); //$NON-NLS-1$
public boolean apply(Match input) {
return input.getLeft() instanceof EAnnotation
&& umlNsPattern.matcher(((EAnnotation)input.getLeft()).getSource()).matches();
}
};
/** UML2 extensions factories. */
private Set<IChangeFactory> uml2ExtensionFactories;
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postMatch(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postMatch(Comparison comparison, Monitor monitor) {
// Check the version of the applied profile on each matched profile application.
boolean isSameProfileVersion = true;
Iterator<Match> matchesRoot = comparison.getMatches().iterator();
while (matchesRoot.hasNext() && isSameProfileVersion) {
Match matchRoot = matchesRoot.next();
isSameProfileVersion = checkProfileVersion(matchRoot);
Iterator<Match> matches = matchRoot.getAllSubmatches().iterator();
while (matches.hasNext() && isSameProfileVersion) {
Match match = matches.next();
isSameProfileVersion = checkProfileVersion(match);
}
}
}
/**
* It checks the profile applications, matched by the given match, are based on the same profile version.
* <br>
* It adds a diagnostic (error) to the comparison as soon as a difference is met.
*
* @param match
* A match of profile applications.
* @return False if a couple of profile applications at least is not based on the same profile version,
* True otherwise.
*/
private boolean checkProfileVersion(Match match) {
EObject left = match.getLeft();
EObject right = match.getRight();
if (left instanceof ProfileApplication && right != null) {
Collection<Match> annotationsMatches = Collections2.filter(match.getSubmatches(),
ANNOTATION_REFERENCING_PROFILE_DEFINITION);
for (Match annotationMatch : annotationsMatches) {
EAnnotation originAnnot = (EAnnotation)annotationMatch.getOrigin();
EAnnotation leftAnnot = (EAnnotation)annotationMatch.getLeft();
EAnnotation rightAnnot = (EAnnotation)annotationMatch.getRight();
if (!checkProfileVersion(match.getComparison(), (ProfileApplication)left, originAnnot,
leftAnnot, rightAnnot)) {
return false;
}
}
}
return true;
}
/**
* It checks the profile annotations reference the same profile version.<br>
* It adds a diagnostic (error) to the comparison as soon as an annotation does not reference the same
* profile version as the annotation from the other side.
*
* @param comparison
* The comparison.
* @param profileApplication
* The profile application to compare (on the left or right side)
* @param originAnnot
* The annotation referencing the profile on the origin side
* @param leftAnnot
* The annotation referencing the profile on the left side
* @param rightAnnot
* The annotation referencing the profile on the right side
* @return False if the version of the referenced profile is different, True otherwise.
*/
private boolean checkProfileVersion(Comparison comparison, ProfileApplication profileApplication,
EAnnotation originAnnot, EAnnotation leftAnnot, EAnnotation rightAnnot) {
Optional<URI> originURI = Optional.absent();
boolean originHasProfileDefinition = false;
if (comparison.isThreeWay()) {
originURI = tryFirst(getNormalizedURIs(
ReferenceUtil.getAsList(originAnnot, EcorePackage.Literals.EANNOTATION__REFERENCES)));
originHasProfileDefinition = getOriginObject(comparison, profileApplication) != null;
}
Optional<URI> leftURI = tryFirst(getNormalizedURIs(
ReferenceUtil.getAsList(leftAnnot, EcorePackage.Literals.EANNOTATION__REFERENCES)));
Optional<URI> rightURI = tryFirst(getNormalizedURIs(
ReferenceUtil.getAsList(rightAnnot, EcorePackage.Literals.EANNOTATION__REFERENCES)));
// If the origin is absent, is doesn't matter: the profile is newly applied to each
// side so any applications of its stereotypes can only be added. But, if the origin
// has a static profile definition, then it's an incompatible change: it should have
// a dynamic definition annotation, so call it 'missing'
boolean missingSomeSide = (leftURI.isPresent() != rightURI.isPresent())
|| (!originURI.isPresent() && originHasProfileDefinition);
if (missingSomeSide || !leftURI.equals(rightURI)
|| (originURI.isPresent() && !originURI.equals(leftURI))) {
// different uri on one of the sides
org.eclipse.uml2.uml.Package impactedPackage = profileApplication.getApplyingPackage();
String message;
if (impactedPackage != null) {
message = UMLCompareMessages.getString("profile.definition.changed.on", "<" //$NON-NLS-1$//$NON-NLS-2$
+ impactedPackage.eClass().getName() + "> " + impactedPackage.getName()); //$NON-NLS-1$
} else {
message = UMLCompareMessages.getString("profile.definition.changed"); //$NON-NLS-1$
}
addDiagnostic(comparison, new BasicDiagnostic(Diagnostic.ERROR, UMLComparePlugin.PLUGIN_ID, 0,
message, new Object[] {}));
return false;
}
return true;
}
/**
* It adds a diagnostic to the given comparison.
*
* @param comparison
* The comparison
* @param diagnostic
* The diagnostic
*/
private void addDiagnostic(Comparison comparison, Diagnostic diagnostic) {
Diagnostic currentDiag = comparison.getDiagnostic();
if (currentDiag == null) {
comparison.setDiagnostic(new BasicDiagnostic(UMLComparePlugin.PLUGIN_ID, 0, null, new Object[0]));
}
((BasicDiagnostic)comparison.getDiagnostic()).merge(diagnostic);
}
/**
* Get the normalized URI of the given ecore objects.
*
* @param eObjects
* The ecore objects.
* @return the list of the URI.
*/
private Collection<URI> getNormalizedURIs(List<Object> eObjects) {
Function<Object, URI> eObjectToURI = new Function<Object, URI>() {
public URI apply(Object input) {
if (input instanceof EObject) {
URI uri = EcoreUtil.getURI((EObject)input);
URIConverter uriConverter = getURIConverter((EObject)input);
if (uriConverter != null) {
uri = uriConverter.normalize(uri);
}
return uri;
}
return null;
}
private URIConverter getURIConverter(EObject eObject) {
Resource resource = eObject.eResource();
if (resource != null) {
ResourceSet resourceSet = resource.getResourceSet();
if (resourceSet != null) {
return resourceSet.getURIConverter();
}
}
return null;
}
};
return Collections2.transform(eObjects, eObjectToURI);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postDiff(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postDiff(Comparison comparison, Monitor monitor) {
// Not needed here.
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postRequirements(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postRequirements(Comparison comparison, Monitor monitor) {
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postEquivalences(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postEquivalences(Comparison comparison, Monitor monitor) {
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postConflicts(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postConflicts(Comparison comparison, Monitor monitor) {
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.postprocessor.IPostProcessor#postComparison(org.eclipse.emf.compare.Comparison,
* org.eclipse.emf.common.util.Monitor)
*/
public void postComparison(Comparison comparison, Monitor monitor) {
final Map<Class<? extends Diff>, IChangeFactory> mapUml2ExtensionFactories = UMLExtensionFactoryRegistry
.createExtensionFactories();
uml2ExtensionFactories = new LinkedHashSet<IChangeFactory>(mapUml2ExtensionFactories.values());
// Creation of the UML difference extensions
List<Diff> differences = comparison.getDifferences();
int diffCount = differences.size();
for (int i = 0; i < diffCount; i++) {
applyManagedTypes(differences.get(i));
reportProgress(monitor, "UMLPostProcessor.monitor.applyManagedTypes", i + 1, diffCount); //$NON-NLS-1$
}
// Filling of the requirements link of the UML difference extensions
// we must not reuse the variable "differences" as applyManagedTypes(Diff) may have added diffs
differences = comparison.getDifferences();
diffCount = differences.size();
for (int i = 0; i < diffCount; i++) {
final Diff umlDiff = differences.get(i);
if (umlDiff instanceof UMLDiff) {
final Class<?> classDiffElement = umlDiff.eClass().getInstanceClass();
final IChangeFactory diffFactory = mapUml2ExtensionFactories.get(classDiffElement);
if (diffFactory != null) {
diffFactory.fillRequiredDifferences(comparison, umlDiff);
}
}
reportProgress(monitor, "UMLPostProcessor.monitor.fillRequiredDifferences", i + 1, diffCount); //$NON-NLS-1$
}
// Filling implications with subsets
// And delete enumeration literal classifier changes, as it is actually a derived feature
for (int i = 0; i < diffCount; i++) {
final Diff diff = differences.get(i);
if (diff instanceof ReferenceChange) {
final ReferenceChange referenceChange = (ReferenceChange)diff;
fillImplicationsWithUMLSubsets(referenceChange);
deleteEnumerationLiteralClassifierChanges(referenceChange);
}
reportProgress(monitor, "UMLPostProcessor.monitor.fillImplications", i + 1, diffCount); //$NON-NLS-1$
}
}
/**
* Reports the progress to the given <code>monitor</code> for the given <code>msgKey</code> in
* {@link UMLCompareMessages} with the <code>currentDiffIndex</code> of the total <code>diffCount</code>.
* This method also checks for cancellation.
*
* @param monitor
* The monitor to report progress.
* @param msgKey
* The message key in {@link UMLCompareMessages}.
* @param currentDiffIndex
* The current diff index.
* @param diffCount
* The total diff count.
*/
private void reportProgress(Monitor monitor, String msgKey, int currentDiffIndex, int diffCount) {
if (currentDiffIndex % 100 == 1) {
monitor.subTask(UMLCompareMessages.getString(msgKey, Integer.valueOf(currentDiffIndex),
Integer.valueOf(diffCount)));
if (monitor.isCanceled()) {
throw new ComparisonCanceledException();
}
}
}
/**
* Creates the difference extensions in relation to the existing {@link DiffElement}s.
*
* @param element
* The input {@link DiffElement}.
*/
private void applyManagedTypes(Diff element) {
for (IChangeFactory factory : uml2ExtensionFactories) {
if (factory.handles(element)) {
Diff extension = factory.create(element);
// FIXME: Instantiation of UML extensions (intersections of predicates)
if (!extension.getRefinedBy().isEmpty()) {
final Match match = factory.getParentMatch(element);
// FIXME: why the match may be null ? (see AddAssociation2Test.testMergeLtRA30UseCase)
if (match != null) {
match.getDifferences().add(extension);
}
} else {
extension = null;
}
}
}
}
/**
* Fill the implication links ({@link Diff#getImplies()}, {@link Diff#getImpliedBy()}) on the given
* reference change.
*
* @param diff
* The reference change.
*/
private void fillImplicationsWithUMLSubsets(ReferenceChange diff) {
boolean isAddOrSet = isAddOrSetDiff(diff);
boolean isDeleteOrUnset = isDeleteOrUnsetDiff(diff);
if (!isAddOrSet && !isDeleteOrUnset) {
return;
}
EReference reference = diff.getReference();
// ADD implies ADD on non union supersets
// DELETE is implied by DEL on non union supersets
for (EReference superSet : UMLCompareUtil.getNonUnionSupersetReferences(reference)) {
Comparison comparison = diff.getMatch().getComparison();
for (Diff superSetDiff : comparison.getDifferences(superSet)) {
// Only keep diffs on the same ref and value where parent matches
if (superSetDiff instanceof ReferenceChange && superSetDiff.getSource() == diff.getSource()
&& ((ReferenceChange)superSetDiff).getReference() == superSet
&& ((ReferenceChange)superSetDiff).getValue() == diff.getValue()
&& superSetDiff.getMatch() == diff.getMatch()) {
if (isAddOrSet && isAddOrSetDiff(superSetDiff)) {
diff.getImplies().add(superSetDiff);
} else if (isDeleteOrUnset && isDeleteOrUnsetDiff(superSetDiff)) {
diff.getImpliedBy().add(superSetDiff);
}
}
}
}
// If we have an eOpposite and we are a containment reference, then we also imply/are implied by the
// changes to this opposite's subset-superset reference.
// "interfaceRealization" is an eOpposite of "implementingClassifier" which in turn is a super set of
// "InterfaceRealization#client". Adding the InterfaceRealization into a Class#interfaceRealizations
// reference implies the setting of this particular realization's "client" reference.
if (reference.isContainment() && reference.getEOpposite() != null) {
for (EReference superSet : UMLCompareUtil
.getNonUnionSupersetReferences(reference.getEOpposite())) {
Comparison comparison = diff.getMatch().getComparison();
for (Diff superSetDiff : comparison.getDifferences(superSet)) {
if (superSetDiff instanceof ReferenceChange
&& superSetDiff.getSource() == diff.getSource()
&& ((ReferenceChange)superSetDiff).getReference() == superSet
&& superSetDiff.getMatch() == comparison.getMatch(diff.getValue())) {
if (isAddOrSet && isAddOrSetDiff(superSetDiff)) {
diff.getImplies().add(superSetDiff);
} else if (isDeleteOrUnset && isDeleteOrUnsetDiff(superSetDiff)) {
diff.getImpliedBy().add(superSetDiff);
}
}
}
}
}
}
/**
* Deletes the given <code>referenceChange</code>, if it is an {@link EnumerationLiteral#getClassifier()
* enumeration literal classifier} change.
*
* @param referenceChange
* The reference change to check and delete.
*/
private void deleteEnumerationLiteralClassifierChanges(ReferenceChange referenceChange) {
if (isEnumerationLiteralClassifierChange(referenceChange)) {
delete(referenceChange);
}
}
/**
* Specifies whether the given <code>referenceChange</code> is a change of an
* {@link EnumerationLiteral#getClassifier() enumeration literal classifier}, whereas the type of the
* reference change's value must be Enumeration.
*
* @param referenceChange
* The reference change to check.
* @return <code>true</code> if it is a EnumerationLiteral classifier change, <code>false</code>
* otherwise.
*/
private boolean isEnumerationLiteralClassifierChange(ReferenceChange referenceChange) {
return INSTANCE_SPECIFICATION__CLASSIFIER.equals(referenceChange.getReference())
&& getAnyMatchedEObject(referenceChange) instanceof EnumerationLiteral
&& referenceChange.getValue() instanceof Enumeration;
}
/**
* Returns the left-, right-, or origin object of the given <code>diff</code>'s match.
*
* @param diff
* The diff to get the matched object for.
* @return The matched object of any side.
*/
private EObject getAnyMatchedEObject(Diff diff) {
final Match match = diff.getMatch();
final EObject eObject;
if (match.getLeft() != null) {
eObject = match.getLeft();
} else if (match.getRight() != null) {
eObject = match.getRight();
} else {
eObject = match.getOrigin();
}
return eObject;
}
}