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