| /******************************************************************************* |
| * Copyright (c) 2015 EclipseSource Munich 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 |
| *******************************************************************************/ |
| package org.eclipse.emf.compare.ide.ui.internal.logical; |
| |
| import static com.google.common.collect.Iterables.concat; |
| import static com.google.common.collect.Iterables.filter; |
| import static com.google.common.collect.Iterables.transform; |
| import static java.util.Arrays.asList; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableList.Builder; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IStorage; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.emf.compare.ide.ui.logical.IStorageProviderAccessor; |
| import org.eclipse.emf.compare.ide.ui.logical.IStorageProviderAccessor.DiffSide; |
| import org.eclipse.team.core.TeamException; |
| import org.eclipse.team.core.diff.IDiff; |
| import org.eclipse.team.core.diff.IThreeWayDiff; |
| import org.eclipse.team.core.subscribers.Subscriber; |
| |
| /** |
| * Detector for revealing potential file renames that may have occurred in {@link DiffSide#SOURCE} or |
| * {@link DiffSide#REMOTE} in the context of a {@link Subscriber}. |
| * |
| * @author Philip Langer <planger@eclipsesource.com> |
| */ |
| public class RenameDetector { |
| |
| /** We don't report progress to the outside at the moment, so we use a static NullProgressMonitor. */ |
| private static final NullProgressMonitor NPM = new NullProgressMonitor(); |
| |
| /** The subscriber for accessing the diffs. */ |
| private final Subscriber subscriber; |
| |
| /** The accessor for accessing the file contents. */ |
| private final IStorageProviderAccessor accessor; |
| |
| /** Cache for affected files. */ |
| private Iterable<IFile> affectedFiles; |
| |
| /** Cache for already computed files before rename on the {@link DiffSide#SOURCE}. */ |
| private Map<IFile, Optional<IFile>> sourceRenameBeforeCache = new HashMap<IFile, Optional<IFile>>(); |
| |
| /** Cache for already computed files after rename on the {@link DiffSide#SOURCE}. */ |
| private Map<IFile, Optional<IFile>> sourceRenameAfterCache = new HashMap<IFile, Optional<IFile>>(); |
| |
| /** Cache for already computed files before rename on the {@link DiffSide#REMOTE}. */ |
| private Map<IFile, Optional<IFile>> remoteRenameBeforeCache = new HashMap<IFile, Optional<IFile>>(); |
| |
| /** Cache for already computed files after rename on the {@link DiffSide#REMOTE}. */ |
| private Map<IFile, Optional<IFile>> remoteRenameAfterCache = new HashMap<IFile, Optional<IFile>>(); |
| |
| /** |
| * Constructor. |
| * |
| * @param subscriber |
| * The subscriber to access the diffs. This parameter may be <code>null</code>, and as such, |
| * will result in no rename detection. |
| * @param accessor |
| * The accessor to access the file variants. |
| */ |
| public RenameDetector(Subscriber subscriber, IStorageProviderAccessor accessor) { |
| this.accessor = Preconditions.checkNotNull(accessor); |
| this.subscriber = subscriber; |
| } |
| |
| /** |
| * Given a source or remote file, this method optionally returns the corresponding {@link IFile} before it |
| * has been renamed on the respective {@code side}, if it has been renamed at all. |
| * <p> |
| * Only {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE} are valid values for {@code side}. |
| * </p> |
| * |
| * @param sourceOrRemoteFile |
| * The potentially renamed file. |
| * @param side |
| * The {@link DiffSide} to look for the rename (only {@link DiffSide#SOURCE} or |
| * {@link DiffSide#REMOTE} are valid). |
| * @return The file before the rename, if it has been renamed at all, {@link Optional#absent()} otherwise. |
| */ |
| public Optional<IFile> getFileBeforeRename(IFile sourceOrRemoteFile, DiffSide side) { |
| Preconditions.checkArgument(isSourceOrRemoteSide(side)); |
| if (!isFileBeforeRenameCached(sourceOrRemoteFile, side)) { |
| cacheFileBeforeRename(computeFileBeforeRename(sourceOrRemoteFile, side), sourceOrRemoteFile, |
| side); |
| } |
| return getCachedFileBeforeRename(sourceOrRemoteFile, side).get(); |
| } |
| |
| /** |
| * Given an origin file, this method optionally returns the corresponding {@link IFile} after it has been |
| * renamed on the respective {@code side}, if it has been renamed at all. |
| * <p> |
| * Only {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE} are valid values for {@code side}. |
| * </p> |
| * |
| * @param originFile |
| * The potentially renamed file. |
| * @param side |
| * The {@link DiffSide} to look for the rename (only {@link DiffSide#SOURCE} or |
| * {@link DiffSide#REMOTE} are valid). |
| * @return The file after the rename, if it has been renamed at all, {@link Optional#absent()} otherwise. |
| */ |
| public Optional<IFile> getFileAfterRename(IFile originFile, DiffSide side) { |
| Preconditions.checkArgument(isSourceOrRemoteSide(side)); |
| if (!isFileAfterRenameCached(originFile, side)) { |
| cacheFileAfterRename(computeFileAfterRename(originFile, side), originFile, side); |
| } |
| return getCachedFileAfterRename(originFile, side).get(); |
| } |
| |
| /** |
| * Specifies whether the result of {@link #computeFileBeforeRename(IFile, DiffSide)} has been cached for |
| * the given {@code sourceOrRemoteFile} and {@code side}. |
| * |
| * @param sourceOrRemoteFile |
| * The source or remote file. |
| * @param side |
| * The side. |
| * @return <code>true</code> if it is cached, <code>false</code> otherwise. |
| */ |
| private boolean isFileBeforeRenameCached(IFile sourceOrRemoteFile, DiffSide side) { |
| return getCachedFileBeforeRename(sourceOrRemoteFile, side).isPresent(); |
| } |
| |
| /** |
| * Caches the result of {@link #computeFileBeforeRename(IFile, DiffSide)} specified as |
| * {@code fileBeforeRename} for the given {@code sourceOrRemoteFile} and {@code side}. |
| * |
| * @param fileBeforeRename |
| * The result to be cached. |
| * @param sourceOrRemoteFile |
| * The input file. |
| * @param side |
| * The input side. |
| */ |
| private void cacheFileBeforeRename(Optional<IFile> fileBeforeRename, IFile sourceOrRemoteFile, |
| DiffSide side) { |
| if (DiffSide.SOURCE.equals(side)) { |
| sourceRenameBeforeCache.put(sourceOrRemoteFile, fileBeforeRename); |
| } else if (DiffSide.REMOTE.equals(side)) { |
| remoteRenameBeforeCache.put(sourceOrRemoteFile, fileBeforeRename); |
| } |
| } |
| |
| /** |
| * Returns the optional cached result of {@link #computeFileBeforeRename(IFile, DiffSide)}. |
| * |
| * @param sourceOrRemoteFile |
| * The source or remote file. |
| * @param side |
| * The side. |
| * @return The cached result, or {@link Optional#absent()} |
| */ |
| private Optional<Optional<IFile>> getCachedFileBeforeRename(IFile sourceOrRemoteFile, DiffSide side) { |
| final Optional<Optional<IFile>> cachedFile; |
| if (DiffSide.SOURCE.equals(side)) { |
| cachedFile = Optional.fromNullable(sourceRenameBeforeCache.get(sourceOrRemoteFile)); |
| } else if (DiffSide.REMOTE.equals(side)) { |
| cachedFile = Optional.fromNullable(remoteRenameBeforeCache.get(sourceOrRemoteFile)); |
| } else { |
| cachedFile = Optional.absent(); |
| } |
| return cachedFile; |
| } |
| |
| /** |
| * Specifies whether the result of {@link #computeFileAfterRename(IFile, DiffSide)} has been cached for |
| * the given {@code originFile} and {@code side}. |
| * |
| * @param originFile |
| * The origin file. |
| * @param side |
| * The side. |
| * @return <code>true</code> if it is cached, <code>false</code> otherwise. |
| */ |
| private boolean isFileAfterRenameCached(IFile originFile, DiffSide side) { |
| return getCachedFileAfterRename(originFile, side).isPresent(); |
| } |
| |
| /** |
| * Caches the result of {@link #computeFileAfterRename(IFile, DiffSide)} specified as |
| * {@code fileAfterRename} for the given {@code originFile} and {@code side}. |
| * |
| * @param fileAfterRename |
| * The result to be cached. |
| * @param originFile |
| * The input file. |
| * @param side |
| * The input side. |
| */ |
| private void cacheFileAfterRename(Optional<IFile> fileAfterRename, IFile originFile, DiffSide side) { |
| if (DiffSide.SOURCE.equals(side)) { |
| sourceRenameAfterCache.put(originFile, fileAfterRename); |
| } else if (DiffSide.REMOTE.equals(side)) { |
| remoteRenameAfterCache.put(originFile, fileAfterRename); |
| } |
| } |
| |
| /** |
| * Returns the optional cached result of {@link #computeFileAfterRename(IFile, DiffSide)}. |
| * |
| * @param originFile |
| * The origin file. |
| * @param side |
| * The side. |
| * @return The cached result, or {@link Optional#absent()} |
| */ |
| private Optional<Optional<IFile>> getCachedFileAfterRename(IFile originFile, DiffSide side) { |
| final Optional<Optional<IFile>> cachedFile; |
| if (DiffSide.SOURCE.equals(side)) { |
| cachedFile = Optional.fromNullable(sourceRenameAfterCache.get(originFile)); |
| } else if (DiffSide.REMOTE.equals(side)) { |
| cachedFile = Optional.fromNullable(remoteRenameAfterCache.get(originFile)); |
| } else { |
| cachedFile = Optional.absent(); |
| } |
| return cachedFile; |
| } |
| |
| /** |
| * Specifies whether {@code side} is either a {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE}. |
| * |
| * @param side |
| * The side to check. |
| * @return <code>true</code> if {@code side} is a {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE}, |
| * <code>false</code> otherwise. |
| */ |
| private boolean isSourceOrRemoteSide(DiffSide side) { |
| return DiffSide.SOURCE.equals(side) || DiffSide.REMOTE.equals(side); |
| } |
| |
| /** |
| * Given a source or remote file, this method optionally returns the corresponding {@link IFile} before it |
| * has been renamed on the respective {@code side}, if it has been renamed at all. |
| * <p> |
| * Only {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE} are valid values for {@code side}. |
| * </p> |
| * |
| * @param sourceOrRemoteFile |
| * The potentially renamed file. |
| * @param side |
| * The {@link DiffSide} to look for the rename (only {@link DiffSide#SOURCE} or |
| * {@link DiffSide#REMOTE} are valid). |
| * @return The file before the rename, if it has been renamed at all, {@link Optional#absent()} otherwise. |
| */ |
| private Optional<IFile> computeFileBeforeRename(IFile sourceOrRemoteFile, DiffSide side) { |
| if (isAddedFile(sourceOrRemoteFile, side)) { |
| for (IFile removedOriginFile : getRemovedFiles(side)) { |
| if (isRename(removedOriginFile, sourceOrRemoteFile, side)) { |
| return Optional.of(removedOriginFile); |
| } |
| } |
| } |
| return Optional.absent(); |
| } |
| |
| /** |
| * Given an origin file, this method optionally returns the corresponding {@link IFile} after it has been |
| * renamed on the respective {@code side}, if it has been renamed at all. |
| * <p> |
| * Only {@link DiffSide#SOURCE} or {@link DiffSide#REMOTE} are valid values for {@code side}. |
| * </p> |
| * |
| * @param originFile |
| * The potentially renamed file. |
| * @param side |
| * The {@link DiffSide} to look for the rename (only {@link DiffSide#SOURCE} or |
| * {@link DiffSide#REMOTE} are valid). |
| * @return The file after the rename, if it has been renamed at all, {@link Optional#absent()} otherwise. |
| */ |
| private Optional<IFile> computeFileAfterRename(IFile originFile, DiffSide side) { |
| if (isRemovedFile(originFile, side)) { |
| for (IFile addedSourceOrRemoteFile : getAddedFiles(side)) { |
| if (isRename(originFile, addedSourceOrRemoteFile, side)) { |
| return Optional.of(addedSourceOrRemoteFile); |
| } |
| } |
| } |
| return Optional.absent(); |
| } |
| |
| /** |
| * Specifies whether the given {@code originFile} has been removed on the given {@code side}. |
| * |
| * @param originFile |
| * The file to check. |
| * @param side |
| * The side to check. |
| * @return <code>true</code> {@code originFile} has been removed, <code>false</code> otherwise. |
| */ |
| private boolean isRemovedFile(IFile originFile, DiffSide side) { |
| return isChangedWithDiffKind(IDiff.REMOVE, side).apply(originFile); |
| } |
| |
| /** |
| * Specifies whether the given {@code originFile} has been added on the given {@code side}. |
| * |
| * @param originFile |
| * The file to check. |
| * @param side |
| * The side to check. |
| * @return <code>true</code> {@code originFile} has been added, <code>false</code> otherwise. |
| */ |
| private boolean isAddedFile(IFile originFile, DiffSide side) { |
| return isChangedWithDiffKind(IDiff.ADD, side).apply(originFile); |
| } |
| |
| /** |
| * Returns all files that have been added on the given {@code side}. |
| * |
| * @param side |
| * The side to get the additions of. |
| * @return The files that have been added. |
| */ |
| private Iterable<IFile> getAddedFiles(DiffSide side) { |
| return filter(getAffectedFiles(), isChangedWithDiffKind(IDiff.ADD, side)); |
| } |
| |
| /** |
| * Returns all files that have been removed on the given {@code side}. |
| * |
| * @param side |
| * The side to get the deletions of. |
| * @return The files that have been removed. |
| */ |
| private Iterable<IFile> getRemovedFiles(DiffSide side) { |
| return filter(getAffectedFiles(), isChangedWithDiffKind(IDiff.REMOVE, side)); |
| } |
| |
| /** |
| * Returns all files that have been affected (i.e., changed in some way). |
| * |
| * @return All files that have been changed. |
| */ |
| private Iterable<IFile> getAffectedFiles() { |
| if (affectedFiles == null) { |
| if (subscriber != null) { |
| final List<IResource> roots = asList(subscriber.roots()); |
| final Iterable<IResource> resources = concat(transform(roots, toAllChildren())); |
| affectedFiles = filter(resources, IFile.class); |
| } else { |
| affectedFiles = Collections.emptySet(); |
| } |
| } |
| return affectedFiles; |
| } |
| |
| /** |
| * Specifies whether the given {@code originFile} should be considered as renamed to |
| * {@code addedSourceOrOriginFile} on the given {@code side} according to their contents. |
| * |
| * @param originFile |
| * The origin file to check. |
| * @param addedSourceOrRemoteFile |
| * The source or remote file to check. |
| * @param side |
| * The side. |
| * @return <code>true</code> if {@code addedSourceOrRemoteFile} at the given {@code side} should be |
| * considered as a renamed version of {@code originFile}, <code>false</code> otherwise. |
| */ |
| private boolean isRename(IFile originFile, IFile addedSourceOrRemoteFile, DiffSide side) { |
| try { |
| final IStorage origin = accessor.getStorageProvider(originFile, DiffSide.ORIGIN).getStorage(NPM); |
| final IStorage added = accessor.getStorageProvider(addedSourceOrRemoteFile, side).getStorage(NPM); |
| if (origin != null && added != null) { |
| return SimilarityComputer.isSimilar(origin.getContents(), added.getContents()); |
| } |
| } catch (CoreException | IOException e) { |
| // can't access a storage so ignore, fall through and return false |
| } |
| return false; |
| } |
| |
| /** |
| * Transforms a {@link IResource} to all of its direct and indirect children. |
| * |
| * @return A function to transform a resource into all its children. |
| */ |
| private Function<IResource, Iterable<IResource>> toAllChildren() { |
| return new Function<IResource, Iterable<IResource>>() { |
| public Iterable<IResource> apply(IResource input) { |
| final Builder<IResource> allChildren = ImmutableList.builder(); |
| try { |
| if (input != null) { |
| for (IResource child : subscriber.members(input)) { |
| allChildren.add(child).addAll(toAllChildren().apply(child)); |
| } |
| } |
| } catch (TeamException | NullPointerException e) { |
| // ignore and fall through |
| // org.eclipse.egit.core.internal.merge.GitResourceVariantCache.members(IResource) |
| // throws NPE if base doesn't contain a folder that exists in source or remote |
| // so ignore and fall through |
| } |
| return allChildren.build(); |
| } |
| }; |
| } |
| |
| /** |
| * Specifies whether an {@link IFile} has been changed with a specified {@code diffKind}. |
| * |
| * @param diffKind |
| * The diff kind to test against. |
| * @param side |
| * The side to test. |
| * @return A predicate for {@link IFile IFiles} that returns <code>true</code>, if the file has been |
| * changed with the specified {@code diffKind} or <code>false</code> otherwise. |
| */ |
| private Predicate<IFile> isChangedWithDiffKind(final int diffKind, final DiffSide side) { |
| return new Predicate<IFile>() { |
| public boolean apply(IFile input) { |
| try { |
| if (input != null && subscriber != null) { |
| final IDiff diff = subscriber.getDiff(input); |
| if (diff != null) { |
| return isDiffKind(diffKind, diff, side); |
| } |
| } |
| } catch (CoreException e) { |
| // ignore and fall through |
| } |
| return false; |
| } |
| }; |
| } |
| |
| /** |
| * Specifies whether a given {@code diff} is a difference of the given {@code diffKind}. Note that we only |
| * consider three-way diffs on the specified side and neglect the {@link IDiff#getKind()}, which may |
| * summarize the diffKind without taking the actual side into account. |
| * |
| * @param diffKind |
| * The difference kind we want to test against. |
| * @param diff |
| * The difference we want to test. |
| * @param side |
| * The side. |
| * @return <code>true</code> if the difference kind of the given {@code diff} is equal to the given |
| * {@code diffKind}, <code>false</code> otherwise. |
| */ |
| private boolean isDiffKind(int diffKind, IDiff diff, DiffSide side) { |
| boolean isDiffKind = false; |
| if (diff instanceof IThreeWayDiff) { |
| final IThreeWayDiff threeWayDiff = (IThreeWayDiff)diff; |
| if (DiffSide.REMOTE.equals(side) && threeWayDiff.getRemoteChange() != null) { |
| isDiffKind = threeWayDiff.getRemoteChange().getKind() == diffKind; |
| } else if (DiffSide.SOURCE.equals(side) && threeWayDiff.getLocalChange() != null) { |
| isDiffKind = threeWayDiff.getLocalChange().getKind() == diffKind; |
| } |
| } |
| return isDiffKind; |
| } |
| } |