/******************************************************************************* * Copyright (c) 2014, 2016 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: * Philip Langer - initial API and implementation * Martin Fleck - bug 507177: consider refinement behavior *******************************************************************************/ package org.eclipse.emf.compare.uml2.internal.merge; import static org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareUtil.getOpaqueElementLanguages; import static org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareUtil.isChangeOfOpaqueElementBodyAttribute; import static org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareUtil.isChangeOfOpaqueElementLanguageAttribute; import com.google.common.base.Optional; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.eclipse.emf.compare.AttributeChange; import org.eclipse.emf.compare.Comparison; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.DifferenceKind; import org.eclipse.emf.compare.DifferenceSource; import org.eclipse.emf.compare.DifferenceState; import org.eclipse.emf.compare.Match; import org.eclipse.emf.compare.internal.utils.DiffUtil; import org.eclipse.emf.compare.merge.AttributeChangeMerger; import org.eclipse.emf.compare.uml2.internal.OpaqueElementBodyChange; import org.eclipse.emf.compare.uml2.internal.postprocessor.util.UMLCompareUtil; import org.eclipse.emf.ecore.EObject; /** * Merger for {@link OpaqueElementBodyChange body changes} of OpaqueActions, OpaqueBehaviors and * OpaqueExpressions. * <p> * This merger handles all {@link Diff differences} that are either {@link OpaqueElementBodyChange opaque * element body changes} themselves or that are {@link AttributeChange attribute changes} refining opaque * element body changes. Note that this merger forces the merging of the entire * {@link OpaqueElementBodyChange}, also if it is invoked only for one {@link AttributeChange} that refines a * {@link OpaqueElementBodyChange}. * </p> * * @author Philip Langer <planger@eclipsesource.com> */ public class OpaqueElementBodyChangeMerger extends AttributeChangeMerger { /** The logger. */ private static final Logger LOGGER = Logger.getLogger(OpaqueElementBodyChangeMerger.class); @Override public boolean isMergerFor(Diff target) { return isOrRefinesOpaqueElementBodyChange(target); } /** * Specifies whether the given {@code target} either is an {@link OpaqueElementBodyChange} or refines an * {@link OpaqueElementBodyChange}. * * @param target * The difference to check. * @return <code>true</code> if it is or refines an {@link OpaqueElementBodyChange}, <code>false</code> * otherwise. */ private boolean isOrRefinesOpaqueElementBodyChange(Diff target) { return getOpaqueElementBodyChange(target).isPresent(); } /** * Returns an the {@code Optional optional} {@link OpaqueElementBodyChange} for the given {@code diff}. * <p> * This is either the {@code diff} itself or the difference that is refined by the given {@code diff}. If * neither {@code diff} itself nor the differences it refines is an {@link OpaqueElementBodyChange}, this * method returns {@code Optional#absent()}. * </p> * * @param diff * The difference to get the {@link OpaqueElementBodyChange} for. * @return The {@link OpaqueElementBodyChange} or {@code Optional#absent()} if not available. */ private Optional<OpaqueElementBodyChange> getOpaqueElementBodyChange(Diff diff) { final Optional<OpaqueElementBodyChange> bodyChange; if (diff instanceof OpaqueElementBodyChange) { bodyChange = Optional.of((OpaqueElementBodyChange)diff); } else if (!diff.getRefines().isEmpty()) { bodyChange = getRefinedOpaqueElementBodyChange(diff); } else { bodyChange = Optional.absent(); } return bodyChange; } /** * Returns the {@link Optional optional} {@link OpaqueElementBodyChange} that is refined by the given * {@code diff} if available. * * @param diff * The difference to get the refined {@link OpaqueElementBodyChange} for. * @return The refined {@link OpaqueElementBodyChange} or {@code Optional#absent()} if not available. */ private Optional<OpaqueElementBodyChange> getRefinedOpaqueElementBodyChange(Diff diff) { for (Diff refinedDiff : diff.getRefines()) { if (refinedDiff instanceof OpaqueElementBodyChange) { return Optional.of((OpaqueElementBodyChange)refinedDiff); } } return Optional.absent(); } /** * Returns true if the complete body change has been merged, i.e., the change and all its refinements. * * @param bodyChange * The {@link OpaqueElementBodyChange} to check. * @return true if the complete body change is merged, false otherwise. */ private boolean isFullyMerged(OpaqueElementBodyChange bodyChange) { if (!isInTerminalState(bodyChange)) { return false; } for (Diff refiningDiff : bodyChange.getRefinedBy()) { if (!isInTerminalState(refiningDiff)) { return false; } } return true; } @Override protected void accept(Diff diff, boolean rightToLeft) { final Optional<OpaqueElementBodyChange> possibleBodyChange = getOpaqueElementBodyChange(diff); if (possibleBodyChange.isPresent()) { final OpaqueElementBodyChange bodyChange = getOpaqueElementBodyChange(diff).get(); // ensure that we do not merge an already MERGED OpaqueElementChange, e.g., when both language and // body try to merge it if (isFullyMerged(bodyChange)) { return; } if (LOGGER.isDebugEnabled()) { int refinedElementCount = bodyChange.getRefinedBy().size(); LOGGER.debug("accept(Diff, boolean) - " + refinedElementCount //$NON-NLS-1$ + " refined diffs to merge for diff " + diff.hashCode()); //$NON-NLS-1$ } switch (bodyChange.getKind()) { case ADD: acceptRefiningDiffs(bodyChange, rightToLeft); break; case DELETE: acceptRefiningDiffs(bodyChange, rightToLeft); break; case CHANGE: changeElement(bodyChange, rightToLeft); break; case MOVE: moveElement(bodyChange, rightToLeft); break; default: break; } // we set the whole refinement diff to merged setFullyMerged(bodyChange, rightToLeft); } } @Override protected void reject(Diff diff, boolean rightToLeft) { final Optional<OpaqueElementBodyChange> possibleBodyChange = getOpaqueElementBodyChange(diff); if (possibleBodyChange.isPresent()) { final OpaqueElementBodyChange bodyChange = possibleBodyChange.get(); // ensure that we do not merge an already MERGED OpaqueElementChange, e.g., when both language and // body try to merge it if (isFullyMerged(bodyChange)) { return; } if (LOGGER.isDebugEnabled()) { int refinedElementCount = bodyChange.getRefinedBy().size(); LOGGER.debug("Reject(Diff, boolean)" + refinedElementCount //$NON-NLS-1$ + " refined diffs to merge for diff " + diff.hashCode()); //$NON-NLS-1$ } switch (bodyChange.getKind()) { case ADD: rejectRefiningDiffs(bodyChange, rightToLeft); break; case DELETE: rejectRefiningDiffs(bodyChange, rightToLeft); break; case CHANGE: changeElement(bodyChange, rightToLeft); break; case MOVE: moveElement(bodyChange, rightToLeft); break; default: break; } // we set the whole refinement diff to merged setFullyMerged(bodyChange, rightToLeft); } } /** * Delegates the accept of the refining differences of the given {@code bodyChange} to the super class ( * {@link AttributeChangeMerger}). * * @param bodyChange * The {@link OpaqueElementBodyChange} to be delegated. * @param rightToLeft * The direction of merging. */ private void acceptRefiningDiffs(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { final List<Diff> sortedRefiningDiffs = sortByMergePriority(bodyChange.getRefinedBy()); for (Diff refiningDiff : sortedRefiningDiffs) { super.accept(refiningDiff, rightToLeft); } } /** * Delegates the reject of the refining differences of the given {@code bodyChange} to the super class ( * {@link AttributeChangeMerger}). * * @param bodyChange * The {@link OpaqueElementBodyChange} to be delegated. * @param rightToLeft * The direction of merging. */ private void rejectRefiningDiffs(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { final List<Diff> sortedRefiningDiffs = sortByMergePriority(bodyChange.getRefinedBy()); for (Diff refiningDiff : sortedRefiningDiffs) { super.reject(refiningDiff, rightToLeft); } } /** * Creates a new list of the given {@code refiningDiffs} sorted by priority of merging. * <p> * The priority of merging is first merge the potentially existing language attribute value differences, * then merge the body attribute value differences. This is important in order to maintain the correct * order of both. We first merge the language attribute value, because #findInsertionIndex(Comparison, * Diff, boolean) works better for language attribute values than for bodies. * </p> * * @param refiningDiffs * The list of refining differences. * @return The sorted list of refining differnces. */ private List<Diff> sortByMergePriority(List<Diff> refiningDiffs) { final LinkedList<Diff> sortedRefiningDiffs = new LinkedList<Diff>(refiningDiffs); Collections.sort(sortedRefiningDiffs, new Comparator<Diff>() { /* * Note: this comparator imposes orderings that are inconsistent with equals. We only want to * ensure that language attribute values are sorted in before body attribute values. */ public int compare(Diff diff1, Diff diff2) { final int compare; if (isChangeOfOpaqueElementLanguageAttribute(diff1) && !isChangeOfOpaqueElementLanguageAttribute(diff2)) { compare = -1; } else if (!isChangeOfOpaqueElementLanguageAttribute(diff1) && isChangeOfOpaqueElementLanguageAttribute(diff2)) { compare = 1; } else { compare = 0; } return compare; } }); return sortedRefiningDiffs; } /** * Performs the change represented by the given {@code bodyChange}. * * @param bodyChange * The {@link OpaqueElementBodyChange} to be performed. * @param rightToLeft * The direction of merging. */ private void changeElement(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { final EObject targetContainer = getTargetContainer(bodyChange.getMatch(), rightToLeft); final String targetValue = getTargetBodyValue(bodyChange, rightToLeft); setBody(targetContainer, targetValue, bodyChange.getLanguage()); } /** * Sets {@code newBody} as the contents of the body for the given {@code language} in the given * {@code container}. * * @param container * The {@link EObject} to set the body. * @param newBody * The content of the body to set. * @param language * The language at which the body shall be set. */ private void setBody(EObject container, String newBody, String language) { final List<String> languages = UMLCompareUtil.getOpaqueElementLanguages(container); final List<String> bodies = UMLCompareUtil.getOpaqueElementBodies(container); final int index = languages.indexOf(language); bodies.set(index, newBody); } /** * Returns the target value, that is, the value to be set when merging the given {@code bodyChange} in the * direction indicated by {@code rightToLeft}. * * @param bodyChange * The bodyChange we are currently merging. * @param rightToLeft * Direction of the merge. * @return The target value to be set when merging. */ private String getTargetBodyValue(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { final String newBody; if (bodyChange.getMatch().getComparison().isThreeWay()) { newBody = performThreeWayTextMerge(bodyChange, rightToLeft); } else if (rightToLeft) { newBody = getRightBodyValue(bodyChange); } else { newBody = getLeftBodyValue(bodyChange); } return newBody; } /** * Performs a three-way text merge for the given {@code bodyChange} and returns the merged text. * <p> * Depending on whether the given {@code bodyChange} is an accept or reject in the context of the merge * direction indicated by {@code rightToLeft}, this method will perform different strategies of merging. * </p> * * @param bodyChange * The bodyChange for which a three-way text diff is to be performed. * @param rightToLeft * The direction of applying the {@code diff}. * @return The merged text. */ private String performThreeWayTextMerge(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { if (isAcceptingChange(bodyChange, rightToLeft)) { return performAcceptingThreeWayTextMerge(bodyChange); } else { return performRejectingThreeWayTextMerge(bodyChange); } } /** * Specifies whether the given {@code diff} is an accept in the context of the given direction of merging * specified in {@code rightToLeft}. * * @param diff * The difference to check. * @param rightToLeft * The direction of the merging. * @return <code>true</code> if it is an accept, <code>false</code> otherwise. */ private boolean isAcceptingChange(Diff diff, boolean rightToLeft) { return (diff.getSource() == DifferenceSource.LEFT && !rightToLeft) || (diff.getSource() == DifferenceSource.RIGHT && rightToLeft); } /** * Performs a three-way text merge accepting the given {@code bodyChange} and returns the merged text. * * @param bodyChange * The bodyChange for which a three-way text diff is to be performed. * @return The merged text. */ private String performAcceptingThreeWayTextMerge(OpaqueElementBodyChange bodyChange) { final String leftBodyValue = getLeftBodyValue(bodyChange); final String rightBodyValue = getRightBodyValue(bodyChange); final String originBodyValue = getOriginBodyValue(bodyChange); return performThreeWayTextMerge(leftBodyValue, rightBodyValue, originBodyValue); } /** * Performs a three-way text merge rejecting the given {@code bodyChange} and returns the merged text. * <p> * The implementation of rejecting a body value change is based on a three-way diff corresponding to * {@link AttributeChangeMerger#performRejectingThreeWayTextMerge(AttributeChange, boolean)}. * </p> * * @param bodyChange * The bodyChange for which a three-way text diff is to be performed. * @return The merged text. */ private String performRejectingThreeWayTextMerge(OpaqueElementBodyChange bodyChange) { final String originBodyValue = getOriginBodyValue(bodyChange); final AttributeChange bodyValueAddition = getBodyValueAddition(bodyChange).get(); final String changedValueFromDiff = (String)bodyValueAddition.getValue(); final String changedValueFromModel; if (DifferenceSource.LEFT.equals(bodyValueAddition.getSource())) { changedValueFromModel = getLeftBodyValue(bodyChange); } else { changedValueFromModel = getRightBodyValue(bodyChange); } return performThreeWayTextMerge(changedValueFromModel, originBodyValue, changedValueFromDiff); } /** * Returns the attribute change that adds a body value from the refining differences of the given * {@code bodyChange}. * * @param bodyChange * The body change to get the body value addition from. * @return The attribute change adding a body value. */ private Optional<AttributeChange> getBodyValueAddition(OpaqueElementBodyChange bodyChange) { for (Diff diff : bodyChange.getRefinedBy()) { if (isChangeOfOpaqueElementBodyAttribute(diff) && DifferenceKind.ADD.equals(diff.getKind())) { return Optional.of((AttributeChange)diff); } } return Optional.absent(); } /** * Returns the body value of the left-hand side that is affected by the given {@code bodyChange}. * * @param bodyChange * The body change to get the left-hand side body value for. * @return The left-hand side body value. */ private String getLeftBodyValue(OpaqueElementBodyChange bodyChange) { final EObject leftContainer = bodyChange.getMatch().getLeft(); return UMLCompareUtil.getOpaqueElementBody(leftContainer, bodyChange.getLanguage()); } /** * Returns the body value of the right-hand side that is affected by the given {@code bodyChange}. * * @param bodyChange * The body change to get the right-hand side body value for. * @return The right-hand side body value. */ private String getRightBodyValue(OpaqueElementBodyChange bodyChange) { final EObject rightContainer = bodyChange.getMatch().getRight(); return UMLCompareUtil.getOpaqueElementBody(rightContainer, bodyChange.getLanguage()); } /** * Returns the body value of the origin that is affected by the given {@code bodyChange}. * * @param bodyChange * The body change to get the origin body value for. * @return The origin body value. */ private String getOriginBodyValue(OpaqueElementBodyChange bodyChange) { final EObject originContainer = bodyChange.getMatch().getOrigin(); return UMLCompareUtil.getOpaqueElementBody(originContainer, bodyChange.getLanguage()); } /** * Performs the move represented by the given {@code bodyChange}. * * @param bodyChange * The {@link OpaqueElementBodyChange} to be performed. * @param rightToLeft * The direction of merging. */ private void moveElement(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { final Match match = bodyChange.getMatch(); final Comparison comparison = match.getComparison(); final String language = bodyChange.getLanguage(); final Diff languageAttributeMove = getLanguageAttributeMove(bodyChange).get(); final EObject container = getTargetContainer(match, rightToLeft); final int sourceIndex = getLanguageIndex(container, language); final int targetIndex = DiffUtil.findInsertionIndex(comparison, languageAttributeMove, rightToLeft); doMove(container, sourceIndex, targetIndex); } /** * Returns the {@link Optional optional} refining {@link AttributeChange} representing the move of the * language attribute value in the given {@code bodyChange}. * * @param bodyChange * The {@link OpaqueElementBodyChange} to get the move difference from. * @return The {@link AttributeChange} representing the move of the language attribute value. */ private Optional<Diff> getLanguageAttributeMove(OpaqueElementBodyChange bodyChange) { for (Diff diff : bodyChange.getRefinedBy()) { if (diff instanceof AttributeChange && DifferenceKind.MOVE.equals(diff.getKind())) { return Optional.of(diff); } } return Optional.absent(); } /** * Returns the target container of the given {@code match} in the context of the direction of the merging * specified in {@code rightToLeft}. * * @param match * The {@link Match} to get the target container from. * @param rightToLeft * The direction of merging. * @return The target container, that is, the object to be changed. */ private EObject getTargetContainer(final Match match, boolean rightToLeft) { if (rightToLeft) { return match.getLeft(); } else { return match.getRight(); } } /** * Returns the index of the given {@code language} in the language values of the given {@code container}. * * @param container * The container to get the index of the language value for. * @param language * The language value. * @return The index of {@code language} in the list of languages in {@code container}. */ private int getLanguageIndex(EObject container, String language) { return UMLCompareUtil.getOpaqueElementLanguages(container).indexOf(language); } /** * Performs the move of the language and body value from the current {@code sourceIndex} to the given * {@code targetIndex}. * * @param container * The container to perform the move in. * @param sourceIndex * The source index specifying the values to be moved. * @param targetIndex * The target index of the move. */ private void doMove(EObject container, int sourceIndex, int targetIndex) { final List<String> languages = UMLCompareUtil.getOpaqueElementLanguages(container); final List<String> bodies = UMLCompareUtil.getOpaqueElementBodies(container); final String bodyValueToMove = bodies.get(sourceIndex); final String languageValueToMove = languages.get(sourceIndex); int insertionIndex = targetIndex; if (sourceIndex < targetIndex) { insertionIndex--; } move(languages, languageValueToMove, insertionIndex); move(bodies, bodyValueToMove, insertionIndex); } /** * Performs the move of the {@code value} in the given {@code list} to the given {@targetIndex}. * * @param list * The list in which the move is to be performed. * @param value * The value to be moved. * @param targetIndex * The target index of the move to be performed. */ private void move(final List<String> list, final String value, int targetIndex) { list.remove(value); if (targetIndex < 0 || targetIndex > list.size()) { list.add(value); } else { list.add(targetIndex, value); } } /** * Sets the {@link DifferenceState state} of all refinement differences, i.e., the body change and its * refining diffs, to merged. * * @param bodyChange * The {@link OpaqueElementBodyChange} to set to merged. * @param rightToLeft * The direction of the merge */ private void setFullyMerged(OpaqueElementBodyChange bodyChange, boolean rightToLeft) { if (isAccepting(bodyChange, rightToLeft)) { bodyChange.setState(DifferenceState.MERGED); for (Diff refiningDiff : bodyChange.getRefinedBy()) { refiningDiff.setState(DifferenceState.MERGED); } } else { bodyChange.setState(DifferenceState.DISCARDED); for (Diff refiningDiff : bodyChange.getRefinedBy()) { refiningDiff.setState(DifferenceState.DISCARDED); } } } @Override public Set<Diff> getDirectMergeDependencies(Diff diff, boolean mergeRightToLeft) { /* * We take care of merging the refining diffs in this merger anyway, so we remove them from the direct * merge dependencies to avoid being called for them again before we even have completed the merge of * the body change itself. */ Set<Diff> dependencies = super.getDirectMergeDependencies(diff, mergeRightToLeft); dependencies.removeAll(diff.getRefinedBy()); return dependencies; } @Override protected int findInsertionIndex(Comparison comparison, Diff diff, boolean rightToLeft) { /* * If body values are added (also if deletions are rejected), we have to make sure that the insertion * index corresponds to the value of the language value they belong to. Therefore, above we made sure * (by sorting differences) that language value changes are applied before body changes and here we * ensure that the body value will be inserted at the index of the language value of the * OpaqueElementBodyChange it belongs to. */ if (shouldUseInsertionIndexOfAffectedLanguage(diff, rightToLeft)) { final EObject expectedContainer = getExpectedContainer(diff, rightToLeft); final Optional<String> language = getAffectedLanguage(diff); return getOpaqueElementLanguages(expectedContainer).indexOf(language.get()); } else { return super.findInsertionIndex(comparison, diff, rightToLeft); } } /** * Returns the container that will be modified by the given {@code diff} in the current merging. This will * be different depending on the merge direction specified in {@code rightToLeft}. * * @param diff * The difference to get the container for. * @param rightToLeft * The direction of the current merge. * @return The expected container. */ private EObject getExpectedContainer(Diff diff, boolean rightToLeft) { final EObject expectedContainer; final Match match = diff.getMatch(); if (rightToLeft) { expectedContainer = match.getLeft(); } else { expectedContainer = match.getRight(); } return expectedContainer; } /** * Returns the {@link Optional optional} language value that is affected by the given {@code diff}. * <p> * The affected language is resolved by obtaining the {@link OpaqueElementBodyChange} that is refined by * the given {@code diff} and returning its {@link OpaqueElementBodyChange#getLanguate() language. Thus, * if the given {@code diff} is not refining an opaque element body change, the language value will be * absent. * * @param diff * The difference to get the corresponding language value for. * @return The language value affected by the given difference. */ private Optional<String> getAffectedLanguage(Diff diff) { final Optional<OpaqueElementBodyChange> bodyChange = getRefinedOpaqueElementBodyChange(diff); if (bodyChange.isPresent()) { return Optional.of(bodyChange.get().getLanguage()); } else { return Optional.absent(); } } /** * Specifies whether we should use the index of the corresponding language value as an insertion index * when merging the given {@code diff}. * <p> * The corresponding language index is the index of the language value that is affected by the given * {@code diff}. This index has to be used if there is an addition of a body value for which we can obtain * a corresponding and existing language value in the expected container. * </p> * * @param diff * The diff to determine whether we should use the index of the affected language. * @param rightToLeft * The direction of merging, used for obtaining expected container * @return <code>true</code> if the affected language value index should be used, <code>false</code> * otherwise. */ private boolean shouldUseInsertionIndexOfAffectedLanguage(Diff diff, boolean rightToLeft) { if (isChangeOfOpaqueElementBodyAttribute(diff)) { final Optional<String> affectedLanguage = getAffectedLanguage(diff); final EObject expectedContainer = getExpectedContainer(diff, rightToLeft); return affectedLanguage.isPresent() && getOpaqueElementLanguages(expectedContainer).contains(affectedLanguage.get()); } else { return false; } } }