/*******************************************************************************
* Copyright (c) 2007, 2015 Zend Technologies 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:
* Zend Technologies - initial API and implementation
*******************************************************************************/
package org.eclipse.php.refactoring.core.move;
import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.dltk.core.*;
import org.eclipse.dltk.internal.corext.refactoring.changes.MoveResourceChange;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.osgi.util.NLS;
import org.eclipse.php.core.ast.nodes.Program;
import org.eclipse.php.core.libfolders.LibraryFolderManager;
import org.eclipse.php.core.libfolders.RenameLibraryFolderChange;
import org.eclipse.php.internal.core.filenetwork.ReferenceTree.Node;
import org.eclipse.php.internal.core.includepath.IncludePath;
import org.eclipse.php.internal.core.includepath.IncludePathManager;
import org.eclipse.php.internal.core.util.collections.BucketMap;
import org.eclipse.php.refactoring.core.PhpRefactoringCoreMessages;
import org.eclipse.php.refactoring.core.changes.ProgramFileChange;
import org.eclipse.php.refactoring.core.changes.RenameBreackpointChange;
import org.eclipse.php.refactoring.core.changes.RenameBuildAndIcludePathChange;
import org.eclipse.php.refactoring.core.changes.RenameConfigurationChange;
import org.eclipse.php.refactoring.core.utils.ASTUtils;
import org.eclipse.php.refactoring.core.utils.RefactoringUtility;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
/**
* Delegate object that contains the logic used by the processor
*
* @author Eden K., 2007
*
*/
@SuppressWarnings("restriction")
public class MoveDelegate {
private PHPMoveProcessor fProcessor;
private IPath fMainDestinationPath;
private Set<IFile> phpFiles;
private BucketMap<IResource, IBreakpoint> fBreakpoints;
private HashMap<IBreakpoint, Map<String, Object>> fBreakpointAttributes;
private List<IBuildpathEntry> oldBuildEntries;
private ArrayList<IBuildpathEntry> newBuildEntries;
private ArrayList<IBuildpathEntry> newIncludePathEntries;
private ArrayList<IBuildpathEntry> oldIncludePath;
public MoveDelegate(PHPMoveProcessor processor) {
fProcessor = processor;
}
/**
* Checks if it is ok to start with the refactoring
*
* @return status
* @throws OperationCanceledException
*/
public RefactoringStatus checkInitialConditions()
throws OperationCanceledException {
IResource[] sourceResources = fProcessor.getSourceSelection();
phpFiles = new HashSet<IFile>();
MoveUtils.getAllPHPFiles(sourceResources, phpFiles);
return new RefactoringStatus();
}
/**
* Final check before the actual refactoring
*
* @return status
* @throws OperationCanceledException
*/
public RefactoringStatus checkFinalConditions()
throws OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();
IContainer destination = fProcessor.getDestination();
IProject sourceProject = fProcessor.getSourceSelection()[0]
.getProject();
IProject destinationProject = destination.getProject();
if (sourceProject != destinationProject)
status.merge(MoveUtils.checkMove(phpFiles, sourceProject,
destination));
// Checks if one of the resources already exists with the same name in
// the destination
IPath dest = fProcessor.getDestination().getFullPath();
IResource[] sourceResources = fProcessor.getSourceSelection();
for (IResource element : sourceResources) {
String newFilePath = dest.toOSString() + File.separatorChar
+ element.getName();
IResource resource = ResourcesPlugin.getWorkspace().getRoot()
.findMember(new Path(newFilePath));
if (resource != null && resource.exists()) {
status.merge(RefactoringStatus.createFatalErrorStatus(NLS.bind(
PhpRefactoringCoreMessages.getString("MoveDelegate.6"), element.getName(), dest.toOSString()))); //$NON-NLS-1$
}
}
return status;
}
/**
* Creates the change for the move action. The change can be a simple change
* (the move itslef) or a more complex change (including references update
* and includes in the file itself) depending on the user selection (update
* references checkbox)
*
* @param pm
* - progress monitor
* @param rootChange
* @return the root change after the additions
* @throws OperationCanceledException
*/
public Change createChange(IProgressMonitor pm, CompositeChange rootChange)
throws CoreException, OperationCanceledException {
fMainDestinationPath = fProcessor.getDestination().getFullPath();
fProcessor.getSourceSelection();
if (!fProcessor.getUpdateReferences()) {
return createSimpleMoveChange(pm, rootChange);
}
return createReferenceUpdatingMoveChange(pm, rootChange);
}
/**
* Adds the move changes to the root change This change is the proper move
* change, nothing else
*
* @param pm
* - progress monitor
* @param rootChange
* - the root change that the new changes are added to
* @return the root change after the additions
*/
private Change createSimpleMoveChange(final IProgressMonitor pm,
final CompositeChange rootChange) throws CoreException,
OperationCanceledException {
try {
pm.beginTask(
PhpRefactoringCoreMessages.getString("MoveDelegate.0"), 100); //$NON-NLS-1$
IResource[] sourceResources = fProcessor.getSourceSelection();
createMoveChange(sourceResources, rootChange);
pm.worked(100);
} finally {
pm.done();
}
return rootChange;
}
/**
* Adds the text and move changes to the root change This change is the a
* more global change, it includes both the file(s) move, the update of it's
* includes and update all the references, all the files that have includes
* to the moved file.
*
* @param pm
* - progress monitor
* @param rootChange
* - the root change that the new changes are added to
* @return the root change after the additions
*/
private Change createReferenceUpdatingMoveChange(IProgressMonitor pm,
CompositeChange rootChange) throws CoreException,
OperationCanceledException {
try {
pm.beginTask(
PhpRefactoringCoreMessages.getString("MoveDelegate.0"), 100); //$NON-NLS-1$
IResource[] sourceResources = fProcessor.getSourceSelection();
createTextChanges(new SubProgressMonitor(pm, 80), rootChange,
phpFiles, sourceResources);
pm.worked(80);
// update configuration file.
createRunConfigurationChange(sourceResources, rootChange);
// There is a tricky thing here.
// The resource move must be happened after text change, and run
// configuration changes(this is because the share file under the
// project)
// but before the other changes, e.g. break point and etc.
createMoveChange(sourceResources, rootChange);
// update associated break point.
createBreakPointChange(sourceResources, rootChange);
createBuildPathChange(sourceResources, rootChange);
createRenameLibraryFolderChange(sourceResources, rootChange);
pm.worked(20);
} finally {
pm.done();
}
return rootChange;
}
private void createRenameLibraryFolderChange(IResource[] sourceResources,
CompositeChange rootChange) {
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
LibraryFolderManager lfm = LibraryFolderManager.getInstance();
for (IResource resource : sourceResources) {
if (resource.getType() == IResource.FOLDER
&& lfm.isInLibraryFolder(resource)) {
IPath newPath = fMainDestinationPath.append(resource.getName());
IFolder newFolder = root.getFolder(newPath);
RenameLibraryFolderChange change = new RenameLibraryFolderChange(
(IFolder) resource, newFolder);
rootChange.add(change);
}
}
}
private void createBuildPathChange(IResource[] sourceResources,
CompositeChange rootChange) throws ModelException {
IResource[] uniqueSourceResources = removeDuplicateResources(sourceResources);
for (IResource element : uniqueSourceResources) {
// only container need handle build/include path.
if (element instanceof IContainer) {
IProject project = element.getProject();
// if moving to another project
if (RefactoringUtility.getResource(fMainDestinationPath)
.getProject() != project) {
removeBuildPath(element, project);
IPath path = element.getFullPath().removeLastSegments(1);
RenameBuildAndIcludePathChange biChange = new RenameBuildAndIcludePathChange(
path, path, element.getName(), "", oldBuildEntries, //$NON-NLS-1$
newBuildEntries, oldIncludePath,
newIncludePathEntries);
if (newBuildEntries.size() > 0
|| newIncludePathEntries.size() > 0) {
rootChange.add(biChange);
}
} else {
updateBuildPath(element, project);
RenameBuildAndIcludePathChange biChange = new RenameBuildAndIcludePathChange(
element.getFullPath().removeLastSegments(1),
fMainDestinationPath, element.getName(),
element.getName(), oldBuildEntries,
newBuildEntries, oldIncludePath,
newIncludePathEntries);
if (newBuildEntries.size() > 0
|| newIncludePathEntries.size() > 0) {
rootChange.add(biChange);
}
}
}
}
}
private void removeBuildPath(IResource resource, IProject project) {
IScriptProject projrct = DLTKCore.create(project);
IPath filePath = resource.getFullPath();
oldBuildEntries = Arrays.asList(projrct.readRawBuildpath());
newBuildEntries = new ArrayList<IBuildpathEntry>();
newBuildEntries.addAll(oldBuildEntries);
for (int i = 0; i < oldBuildEntries.size(); i++) {
IBuildpathEntry fEntryToChange = oldBuildEntries.get(i);
IPath entryPath = fEntryToChange.getPath();
int mattchedPath = entryPath.matchingFirstSegments(filePath);
if (mattchedPath == filePath.segmentCount()) {
newBuildEntries.remove(fEntryToChange);
} else {
IBuildpathEntry newEntry = RefactoringUtility
.createNewBuildpathEntry(fEntryToChange,
fEntryToChange.getPath(), filePath, ""); //$NON-NLS-1$
newBuildEntries.remove(fEntryToChange);
newBuildEntries.add(newEntry);
}
}
oldIncludePath = new ArrayList<IBuildpathEntry>();
newIncludePathEntries = new ArrayList<IBuildpathEntry>();
List<IncludePath> includePathEntries = Arrays.asList(IncludePathManager
.getInstance().getIncludePaths(project));
for (IncludePath entry : includePathEntries) {
Object includePathEntry = entry.getEntry();
IResource includeResource = null;
if (!(includePathEntry instanceof IBuildpathEntry)) {
includeResource = (IResource) includePathEntry;
IPath entryPath = includeResource.getFullPath();
IBuildpathEntry oldEntry = RefactoringUtility
.createNewBuildpathEntry(IBuildpathEntry.BPE_SOURCE,
entryPath);
oldIncludePath.add((IBuildpathEntry) oldEntry);
if (filePath.isPrefixOf(entryPath)
|| entryPath.equals(filePath)) {
} else {
IBuildpathEntry newEntry = RefactoringUtility
.createNewBuildpathEntry(
IBuildpathEntry.BPE_SOURCE, entryPath);
newIncludePathEntries.add(newEntry);
}
} else {
newIncludePathEntries.add((IBuildpathEntry) includePathEntry);
oldIncludePath.add((IBuildpathEntry) includePathEntry);
}
}
}
private void updateBuildPath(IResource resource, IProject project) {
String newElementName = resource.getName();
IScriptProject projrct = DLTKCore.create(project);
IPath filePath = resource.getFullPath();
oldBuildEntries = Arrays.asList(projrct.readRawBuildpath());
newBuildEntries = new ArrayList<IBuildpathEntry>();
newBuildEntries.addAll(oldBuildEntries);
for (int i = 0; i < oldBuildEntries.size(); i++) {
IBuildpathEntry fEntryToChange = oldBuildEntries.get(i);
IPath entryPath = fEntryToChange.getPath();
int mattchedPath = entryPath.matchingFirstSegments(filePath);
if (mattchedPath == filePath.segmentCount()) {
newBuildEntries.remove(fEntryToChange);
} else {
IBuildpathEntry newEntry = RefactoringUtility
.createNewBuildpathEntry(fEntryToChange,
fEntryToChange.getPath(), filePath, ""); //$NON-NLS-1$
newBuildEntries.remove(fEntryToChange);
newBuildEntries.add(newEntry);
}
}
oldIncludePath = new ArrayList<IBuildpathEntry>();
newIncludePathEntries = new ArrayList<IBuildpathEntry>();
List<IncludePath> includePathEntries = Arrays.asList(IncludePathManager
.getInstance().getIncludePaths(project));
for (IncludePath entry : includePathEntries) {
Object includePathEntry = entry.getEntry();
IResource includeResource = null;
if (!(includePathEntry instanceof IBuildpathEntry)) {
includeResource = (IResource) includePathEntry;
IPath entryPath = includeResource.getFullPath();
IBuildpathEntry oldEntry = RefactoringUtility
.createNewBuildpathEntry(IBuildpathEntry.BPE_SOURCE,
entryPath);
oldIncludePath.add((IBuildpathEntry) oldEntry);
if (filePath.isPrefixOf(entryPath)
|| entryPath.equals(filePath)) {
int mattchedPath = entryPath
.matchingFirstSegments(filePath);
IPath truncatedPath = entryPath.uptoSegment(mattchedPath);
IPath remaingPath = entryPath
.removeFirstSegments(mattchedPath);
IPath newPath;
if (mattchedPath == filePath.segmentCount()) {
newPath = truncatedPath.removeLastSegments(1)
.append(newElementName).append(remaingPath);
} else {
newPath = truncatedPath.append(newElementName).append(
remaingPath);
}
IBuildpathEntry newEntry = RefactoringUtility
.createNewBuildpathEntry(
IBuildpathEntry.BPE_SOURCE, newPath);
newIncludePathEntries.add(newEntry);
} else {
IBuildpathEntry newEntry = RefactoringUtility
.createNewBuildpathEntry(
IBuildpathEntry.BPE_SOURCE, entryPath);
newIncludePathEntries.add(newEntry);
}
} else {
newIncludePathEntries.add((IBuildpathEntry) includePathEntry);
oldIncludePath.add((IBuildpathEntry) includePathEntry);
}
}
}
private void createBreakPointChange(IResource[] sourceResources,
CompositeChange rootChange) throws CoreException {
IResource[] uniqueSourceResources = removeDuplicateResources(sourceResources);
for (IResource element : uniqueSourceResources) {
collectBrakePoint(element);
RenameBreackpointChange breakePointchanges = new RenameBreackpointChange(
element.getFullPath().removeLastSegments(1),
fMainDestinationPath, element.getName(), element.getName(),
fBreakpoints, fBreakpointAttributes);
if (fBreakpoints.getKeys().size() > 0) {
rootChange.add(breakePointchanges);
}
}
}
protected void collectBrakePoint(IResource resource) throws CoreException {
fBreakpoints = new BucketMap<IResource, IBreakpoint>(6);
fBreakpointAttributes = new HashMap<IBreakpoint, Map<String, Object>>(6);
final IBreakpointManager breakpointManager = DebugPlugin.getDefault()
.getBreakpointManager();
IMarker[] markers = resource.findMarkers(
IBreakpoint.LINE_BREAKPOINT_MARKER, true,
IResource.DEPTH_INFINITE);
for (IMarker marker : markers) {
IResource markerResource = marker.getResource();
IBreakpoint breakpoint = breakpointManager.getBreakpoint(marker);
if (breakpoint != null) {
fBreakpoints.add(markerResource, breakpoint);
fBreakpointAttributes.put(breakpoint, breakpoint.getMarker()
.getAttributes());
}
}
}
private void createRunConfigurationChange(IResource[] sourceResources,
CompositeChange rootChange) {
IResource[] uniqueSourceResources = removeDuplicateResources(sourceResources);
for (IResource element : uniqueSourceResources) {
RenameConfigurationChange configPointchanges = new RenameConfigurationChange(
element.getFullPath().removeLastSegments(1),
fMainDestinationPath, element.getName(), element.getName());
rootChange.add(configPointchanges);
}
}
/**
* Creates the text changes for all the affected files. Updates all the
* include statements in the current file and all the includes in the
* "including " files. In case of folders, creates the changes recursively
*
* @param pm
* - progress monitor
* @param rootChange
* - the root change that the new changes are added to
* @param sourceResources
* @return the root change after the additions
* @throws CoreException
*/
private Change createTextChanges(IProgressMonitor pm,
CompositeChange rootChange, Set<IFile> phpFiles,
IResource[] sourceResources) throws CoreException {
List<ProgramFileChange> changes = new ArrayList<ProgramFileChange>();
try {
pm.beginTask(
PhpRefactoringCoreMessages.getString("MoveDelegate.1"), 100); //$NON-NLS-1$
// creat text changes:
// for each file that will be moved, update its includes
// and update all the files that include it,
IResource[] uniqueSourceResources = removeDuplicateResources(sourceResources);
for (Iterator<IFile> it = phpFiles.iterator(); it.hasNext();) {
IFile currentMovedResource = it.next();
Map<IFile, Program> participantFiles = collectReferencingFiles(
currentMovedResource, pm);
for (Entry<IFile, Program> entry : participantFiles.entrySet()) {
final IFile file = entry.getKey();
if (phpFiles.contains(file)) {
continue;
}
final Program program = entry.getValue();
final ChangeIncludePath rename = new ChangeIncludePath(
currentMovedResource, file, fMainDestinationPath,
false, uniqueSourceResources);
// aggregate the changes identifiers
program.accept(rename);
if (pm.isCanceled())
throw new OperationCanceledException();
pm.worked(1);
if (rename.hasChanges()) {
ProgramFileChange change = new ProgramFileChange(
file.getName(), file, program);
change.setEdit(new MultiTextEdit());
change.setTextType("php"); //$NON-NLS-1$
changes.add(change);
rename.updateChange(change);
}
}
ISourceModule sourceModule = DLTKCore
.createSourceModuleFrom(currentMovedResource);
if (sourceModule instanceof ISourceModule) {
Program program = null;
try {
program = ASTUtils
.createProgramFromSource(sourceModule);
} catch (Exception e) {
}
if (program != null) {
final ChangeIncludePath rename = new ChangeIncludePath(
currentMovedResource, currentMovedResource,
fMainDestinationPath, true,
uniqueSourceResources);
// aggregate the changes identifiers
program.accept(rename);
if (pm.isCanceled())
throw new OperationCanceledException();
pm.worked(1);
if (rename.hasChanges()) {
ProgramFileChange change = new ProgramFileChange(
currentMovedResource.getName(),
currentMovedResource, program);
change.setEdit(new MultiTextEdit());
change.setTextType("php"); //$NON-NLS-1$
changes.add(change);
rename.updateChange(change);
}
}
}
}
pm.worked(70);
} finally {
pm.done();
}// getChildren()
Map<IFile, List<TextEdit>> changeMap = new HashMap<IFile, List<TextEdit>>();
Map<IFile, ProgramFileChange> fileMap = new HashMap<IFile, ProgramFileChange>();
for (ProgramFileChange programFileChange : changes) {
List<TextEdit> list = changeMap.get(programFileChange.getFile());
if (list == null) {
list = new ArrayList<TextEdit>();
changeMap.put(programFileChange.getFile(), list);
fileMap.put(programFileChange.getFile(), programFileChange);
} else {
}
list.addAll(Arrays
.asList(programFileChange.getEdit().getChildren()));
}
for (IFile file : changeMap.keySet()) {
ProgramFileChange change = new ProgramFileChange(file.getName(),
file, fileMap.get(file).getProgram());
change.setEdit(new MultiTextEdit());
change.setTextType("php"); //$NON-NLS-1$
List<TextEdit> list = changeMap.get(file);
Collections.sort(list, new Comparator<TextEdit>() {
public int compare(TextEdit o1, TextEdit o2) {
return o2.getOffset() - o1.getOffset();
}
});
for (TextEdit textEdit : list) {
if (textEdit instanceof ReplaceEdit) {
ReplaceEdit replaceEdit = (ReplaceEdit) textEdit;
change.addEdit(new ReplaceEdit(replaceEdit.getOffset(),
replaceEdit.getLength(), replaceEdit.getText()));
}
}
rootChange.add(change);
}
return rootChange;
}
private IResource[] removeDuplicateResources(IResource[] sourceResources) {
// ignore empty array
if (sourceResources == null || sourceResources.length == 0) {
return sourceResources;
}
ArrayList<IResource> result = new ArrayList<IResource>();
for (IResource source : sourceResources) {
if (result.size() == 0) {
result.add(source);
} else {
// check if the resource is parent of any item in the result
for (IResource existing : result) {
// if the resource is parent of an existing item in the
// result.
// remove the existing item, add the new one.
if (source.getFullPath().isPrefixOf(existing.getFullPath())) {
result.remove(existing);
result.add(source);
}
}
boolean noNeedAdded = false;
for (IResource existing : result) {
// if the resource is parent of an existing item in the
// result.
// remove the existing item, add the new one.
if (existing.getFullPath().isPrefixOf(source.getFullPath())) {
noNeedAdded = true;
}
}
// the source is not in the result after loop
if (!result.contains(source) && !noNeedAdded) {
result.add(source);
}
}
}
IResource[] ret = new IResource[result.size()];
return result.toArray(ret);
}
/**
* Add move changes for each for the selected resources
*
* @param sourceResources
* @param rootChange
*/
private void createMoveChange(IResource[] sourceResources,
CompositeChange rootChange) {
IResource[] uniqueSourceResources = removeDuplicateResources(sourceResources);
for (IResource element : uniqueSourceResources) {
MoveResourceChange moveResource = new MoveResourceChange(element,
fProcessor.getDestination());
rootChange.add(moveResource);
}
}
/**
* Checks whether the given move included is vaild
*
* @param destination
* @return a staus indicating whether the included is valid or not
*/
public RefactoringStatus verifyDestination(IResource destination) {
RefactoringStatus status = new RefactoringStatus();
Assert.isNotNull(destination);
if (!destination.exists() || destination.isPhantom())
return RefactoringStatus
.createFatalErrorStatus(PhpRefactoringCoreMessages
.getString("MoveDelegate.2")); //$NON-NLS-1$
if (!destination.isAccessible())
return RefactoringStatus
.createFatalErrorStatus(PhpRefactoringCoreMessages
.getString("MoveDelegate.3")); //$NON-NLS-1$
Assert.isTrue(destination.getType() != IResource.ROOT);
IResource[] sourceResources = fProcessor.getSourceSelection();
for (IResource element : sourceResources) {
if (destination.equals(element.getParent()))
return RefactoringStatus
.createFatalErrorStatus(PhpRefactoringCoreMessages
.getString("MoveDelegate.4")); //$NON-NLS-1$
if (destination.equals(element)) {
return RefactoringStatus
.createFatalErrorStatus(PhpRefactoringCoreMessages
.getString("MoveDelegate.5")); //$NON-NLS-1$
}
}
return status;
}
private Map<IFile, Program> collectReferencingFiles(IFile sourceFile,
IProgressMonitor pm) {
ISourceModule sourceModule = DLTKCore
.createSourceModuleFrom(sourceFile);
Map<IFile, Program> participantFiles = new HashMap<IFile, Program>();
Collection<Node> references = MoveUtils
.getReferencingFiles(sourceModule);
if (references != null) {
for (Iterator<Node> it = references.iterator(); it.hasNext();) {
Node node = it.next();
IFile file = (IFile) node.getFile().getResource();
try {
participantFiles.put(file,
RefactoringUtility.getProgramForFile(file));
} catch (Exception e) {
}
}
}
return participantFiles;
}
}