blob: 9ea7fc761921b908a295ac43cf4e4f357c7bc245 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012 Obeo.
* 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
*******************************************************************************/
package org.eclipse.emf.compare.utils;
import com.google.common.collect.Iterables;
import java.io.PrintStream;
import java.util.Arrays;
import org.eclipse.emf.compare.AttributeChange;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceKind;
import org.eclipse.emf.compare.DifferenceSource;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.MatchResource;
import org.eclipse.emf.compare.ReferenceChange;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EObject;
// TODO do we need to externalize this? For now, suppressing the NLS warnings.
/**
* This class exposes methods to serialize a "human-readable" form of the comparison model onto a given
* stream.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
@SuppressWarnings("nls")
public final class EMFComparePrettyPrinter {
/** This is the max length of the columns we display for the Match. */
private static final int COLUMN_LENGTH = 40;
/**
* Hides default constructor.
*/
private EMFComparePrettyPrinter() {
// No need to construct an instance of this.
}
/**
* Prints the whole comparison on the given stream (might be {@code stream}).
*
* @param comparison
* The comparison we are to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print this comparison model.
*/
public static void printComparison(Comparison comparison, PrintStream stream) {
for (MatchResource res : comparison.getMatchedResources()) {
stream.println("Matched resources :");
stream.println("Left = " + res.getLeftURI());
stream.println("Right = " + res.getRightURI());
stream.println("origin = " + res.getOriginURI());
}
stream.println();
printMatch(comparison, stream);
stream.println();
printDifferences(comparison, stream);
}
/**
* Prints all the Match elements contained by the given {@code comparison}. Each Match will be displayed
* on its own line.
* <p>
* For example, if the left model has two packages "package1" and "package2", but the right has "package1"
* and "package3", what we will display here depends on the Match : if "left.package1" is matched with
* "right.package1", but "package2" and "package3" did not match, this will print <code><pre>
* | package1 | package1 |
* | package2 | |
* | | package3 |
* </pre></code> On the contrary, if "package2" and "package3" did match, we will display <code><pre>
* | package1 | package1 |
* | package2 | package3 |
* </pre></code>
* </p>
*
* @param comparison
* The comparison which Matched elements we are to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print the matched elements of this comparison.
*/
public static void printMatch(Comparison comparison, PrintStream stream) {
final String separator = "+----------------------------------------+----------------------------------------+----------------------------------------+";
final String leftLabel = "Left";
final String rightLabel = "Right";
final String originLabel = "Origin";
stream.println(separator);
stream.println('|' + formatHeader(leftLabel) + '|' + formatHeader(rightLabel) + '|'
+ formatHeader(originLabel) + '|');
stream.println(separator);
for (Match match : comparison.getMatches()) {
printMatch(match, stream);
}
stream.println(separator);
}
/**
* Prints all differences detected for the given {@code comparison} on the given {@code stream}.
*
* @param comparison
* The comparison which differences we are to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print these differences.
*/
public static void printDifferences(Comparison comparison, PrintStream stream) {
final Iterable<ReferenceChange> refChanges = Iterables.filter(comparison.getDifferences(),
ReferenceChange.class);
stream.println("REFERENCE CHANGES");
for (Diff diff : refChanges) {
printDiff(diff, stream);
}
stream.println();
stream.println("ATTRIBUTE CHANGES");
final Iterable<AttributeChange> attChanges = Iterables.filter(comparison.getDifferences(),
AttributeChange.class);
for (Diff diff : attChanges) {
printDiff(diff, stream);
}
stream.println();
stream.println("CONFLICTS");
for (Conflict conflict : comparison.getConflicts()) {
printConflict(conflict, stream);
}
}
/**
* Prints the given {@link Conflict} on the given {@code stream}.
*
* @param conflict
* The conflict we need to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print this conflict.
*/
private static void printConflict(Conflict conflict, PrintStream stream) {
stream.println(conflict.getKind() + " conflict:");
final Iterable<ReferenceChange> refChanges = Iterables.filter(conflict.getDifferences(),
ReferenceChange.class);
for (Diff diff : refChanges) {
stream.print("\t");
printDiff(diff, stream);
}
final Iterable<AttributeChange> attChanges = Iterables.filter(conflict.getDifferences(),
AttributeChange.class);
for (Diff diff : attChanges) {
stream.print("\t");
printDiff(diff, stream);
}
}
/**
* Prints the given {@link Diff difference} on the given {@code stream}.
*
* @param diff
* The difference we are to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print this difference.
*/
private static void printDiff(Diff diff, PrintStream stream) {
if (diff instanceof ReferenceChange) {
final ReferenceChange refChange = (ReferenceChange)diff;
final String valueName;
if (refChange.getValue() instanceof ENamedElement) {
valueName = ((ENamedElement)refChange.getValue()).getName();
} else {
valueName = refChange.getValue().toString();
}
String change = "";
if (diff.getSource() == DifferenceSource.RIGHT) {
change = "remotely ";
}
if (diff.getKind() == DifferenceKind.ADD) {
change += "added to";
} else if (diff.getKind() == DifferenceKind.DELETE) {
change += "deleted from";
} else if (diff.getKind() == DifferenceKind.CHANGE) {
change += "changed from";
} else {
change += "moved from";
}
final String objectName;
if (refChange.getMatch().getLeft() instanceof ENamedElement) {
objectName = ((ENamedElement)refChange.getMatch().getLeft()).getName();
} else if (refChange.getMatch().getRight() instanceof ENamedElement) {
objectName = ((ENamedElement)refChange.getMatch().getRight()).getName();
} else if (refChange.getMatch().getOrigin() instanceof ENamedElement) {
objectName = ((ENamedElement)refChange.getMatch().getOrigin()).getName();
} else {
objectName = "";
}
stream.println("value " + valueName + " has been " + change + " reference "
+ refChange.getReference().getName() + " of object " + objectName);
} else if (diff instanceof AttributeChange) {
final AttributeChange attChange = (AttributeChange)diff;
String valueName = "null";
if (attChange.getValue() != null) {
valueName = attChange.getValue().toString();
}
String change = "";
if (diff.getSource() == DifferenceSource.RIGHT) {
change = "remotely ";
}
if (diff.getKind() == DifferenceKind.ADD) {
change += "added to";
} else if (diff.getKind() == DifferenceKind.DELETE) {
change += "deleted from";
} else if (diff.getKind() == DifferenceKind.CHANGE) {
change += "changed from";
} else {
change += "moved from";
}
final String objectName;
if (attChange.getMatch().getLeft() instanceof ENamedElement) {
objectName = ((ENamedElement)attChange.getMatch().getLeft()).getName();
} else if (attChange.getMatch().getRight() instanceof ENamedElement) {
objectName = ((ENamedElement)attChange.getMatch().getRight()).getName();
} else if (attChange.getMatch().getOrigin() instanceof ENamedElement) {
objectName = ((ENamedElement)attChange.getMatch().getOrigin()).getName();
} else {
objectName = "";
}
stream.println("value " + valueName + " has been " + change + " attribute "
+ attChange.getAttribute().getName() + " of object " + objectName);
}
}
/**
* Prints the given {@link Match} on the given {@code stream}.
*
* @param match
* The match we are to print on {@code stream}.
* @param stream
* The {@link PrintStream} on which we should print this difference.
* @see #printMatch(Comparison, PrintStream) A description on how we format the match.
*/
private static void printMatch(Match match, PrintStream stream) {
String leftName = null;
String rightName = null;
String originName = null;
final EObject left = match.getLeft();
final EObject right = match.getRight();
final EObject origin = match.getOrigin();
// Ignore this match if it is not a Named element
if (isNullOrNamedElement(left) && isNullOrNamedElement(right) && isNullOrNamedElement(origin)) {
if (left != null && ((ENamedElement)left).getName() != null) {
leftName = formatName((ENamedElement)left);
} else {
int level = 0;
EObject currentMatch = match;
while (currentMatch instanceof Match && ((Match)currentMatch).getLeft() == null) {
currentMatch = currentMatch.eContainer();
}
while (currentMatch instanceof Match && ((Match)currentMatch).getLeft() != null) {
level++;
currentMatch = currentMatch.eContainer();
}
leftName = getEmptyLine(level);
}
if (right != null && ((ENamedElement)right).getName() != null) {
rightName = formatName((ENamedElement)right);
} else {
int level = 0;
EObject currentMatch = match;
while (currentMatch instanceof Match && ((Match)currentMatch).getRight() == null) {
currentMatch = currentMatch.eContainer();
}
while (currentMatch instanceof Match && ((Match)currentMatch).getRight() != null) {
level++;
currentMatch = currentMatch.eContainer();
}
rightName = getEmptyLine(level);
}
if (origin != null && ((ENamedElement)origin).getName() != null) {
originName = formatName((ENamedElement)origin);
} else {
int level = 0;
EObject currentMatch = match;
while (currentMatch instanceof Match && ((Match)currentMatch).getOrigin() == null) {
currentMatch = currentMatch.eContainer();
}
while (currentMatch instanceof Match && ((Match)currentMatch).getOrigin() != null) {
level++;
currentMatch = currentMatch.eContainer();
}
originName = getEmptyLine(level);
}
stream.println('|' + leftName + '|' + rightName + '|' + originName + '|');
}
for (Match submatch : match.getSubmatches()) {
printMatch(submatch, stream);
}
}
/**
* Formats the given header so that it spans {@value #COLUMN_LENGTH} characters, centered between white
* spaces.
*
* @param header
* The header we are to format.
* @return The formatted header.
*/
private static String formatHeader(String header) {
int padding = (COLUMN_LENGTH - header.length()) / 2;
char[] charsBefore = new char[padding];
for (int i = 0; i < charsBefore.length; i++) {
charsBefore[i] = ' ';
}
if ((header.length() & 1) == 1) {
padding++;
}
final char[] charsAfter = new char[padding];
for (int i = 0; i < charsAfter.length; i++) {
charsAfter[i] = ' ';
}
return String.valueOf(charsBefore) + header + String.valueOf(charsAfter);
}
/**
* Formats the named of the given element by adding spaces before and after it so that it spans
* {@value #COLUMN_LENGTH} characters at most.
*
* @param element
* The element which name should be formatted.
* @return the formatted element's name.
*/
private static String formatName(ENamedElement element) {
String name = element.getName();
int level = 0;
EObject current = element;
while (current.eContainer() != null) {
level++;
current = current.eContainer();
}
char[] charsBefore = new char[1 + (level * 2)];
charsBefore[0] = ' ';
if (level > 0) {
for (int i = 1; i < charsBefore.length - 2; i = i + 2) {
charsBefore[i] = '|';
charsBefore[i + 1] = ' ';
}
charsBefore[charsBefore.length - 2] = '|';
charsBefore[charsBefore.length - 1] = '-';
}
int missingChars = COLUMN_LENGTH - name.length() - charsBefore.length;
final char[] spacesAfter = new char[Math.max(0, missingChars)];
Arrays.fill(spacesAfter, ' ');
return String.valueOf(charsBefore) + name + String.valueOf(spacesAfter);
}
/**
* Returns an "empty line" which will only show pipes for previous levels.
*
* @param level
* The level of nesting that we should make visible through pipes on this line.
* @return A line that displays only pipes for a tree's {@code level}, and only that.
*/
private static String getEmptyLine(int level) {
char[] charsBefore = new char[1 + (level * 2)];
charsBefore[0] = ' ';
for (int i = 1; i < charsBefore.length; i = i + 2) {
charsBefore[i] = '|';
charsBefore[i + 1] = ' ';
}
int missingChars = COLUMN_LENGTH - charsBefore.length;
final char[] spacesAfter = new char[Math.max(0, missingChars)];
Arrays.fill(spacesAfter, ' ');
return String.valueOf(charsBefore) + String.valueOf(spacesAfter);
}
/**
* Returns <code>true</code> if the given {@code object} is either <code>null</code> or an instance of
* {@link ENamedElement}.
*
* @param object
* The object we'll test here.
* @return <code>true</code> if the given {@code object} is either <code>null</code> or an instance of
* {@link ENamedElement}.
*/
private static boolean isNullOrNamedElement(final Object object) {
return object == null || object instanceof ENamedElement;
}
}