/*******************************************************************************
* This file is protected by Copyright.
* Please refer to the COPYRIGHT file distributed with this source distribution.
*
* This file is part of REDHAWK IDE.
*
* 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
*******************************************************************************/
package gov.redhawk.ide.ui.action;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange;
import org.eclipse.search.ui.text.FileTextSearchScope;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.statushandlers.StatusManager;
import gov.redhawk.ide.natures.ScaComponentProjectNature;
import gov.redhawk.ide.natures.ScaNodeProjectNature;
import gov.redhawk.ide.ui.RedhawkIDEUiPlugin;
import mil.jpeojtrs.sca.spd.SpdPackage;
/**
* @since 11.0
*/
@SuppressWarnings("restriction")
public class RenameFileParticipant extends RenameParticipant {
/** The project that is being refactored */
private IProject currentProject;
/** Keep a list of active editors so that we later reference it */
private IEditorReference[] activeEditors;
/**
* The current project's editor input
* Used to find out if a particular editor matches up with the spd we have on file
*/
private FileEditorInput input;
@Override
protected boolean initialize(final Object element) {
/* Verify object for refactoring is a project with the REDHAWK component nature.
* Prompt to save, then close associated editors in the UI thread, otherwise conflicts
* will arise when we start making changes to resources.
* Finally, refresh the local resource to make Eclipse full aware of our changes.
*/
if (!(element instanceof IProject)) {
return false;
}
this.currentProject = (IProject) element;
try {
if (!hasCorrectNature(currentProject)) {
return false;
}
// Prompt to save any dirty editors associated with this project
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
IDE.saveAllEditors(new IResource[] { RenameFileParticipant.this.currentProject }, true);
RenameFileParticipant.this.activeEditors = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getEditorReferences();
}
});
// If the user decided not to save, we won't try and update anything other than the Project name
for (IEditorReference editor : activeEditors) {
if (editor.isDirty()) {
return false;
}
}
// Close associated editors in the UI thread, otherwise conflicts will arise when we start making changes to
// resources.
for (final IEditorReference editor : this.activeEditors) {
final IEditorPart editorPart = editor.getEditor(false);
if (editor.getEditorInput() instanceof FileEditorInput) {
this.input = (FileEditorInput) editor.getEditorInput();
if (this.currentProject.equals(this.input.getFile().getProject())) {
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
editor.getPage().closeEditor(editorPart, true);
}
});
}
}
}
// Refresh the local resource to make Eclipse full aware of our changes.
this.currentProject.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
return true;
} catch (final CoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, RedhawkIDEUiPlugin.PLUGIN_ID, "Failed to perform project refactor", e),
StatusManager.SHOW | StatusManager.LOG);
return false;
}
}
/*
* Loosely based on DevDaily's MyRenameTypeParticipant example.
* Start by searching the current project for all instances of the oldname, including folders and files.
* Create changes for replacing instances of oldname with the newname in files, then folders as order is important.
* Return the result.
*/
@Override
public Change createPreChange(final IProgressMonitor pm) throws CoreException {
final RenameFileSearchRequestor searchRequestor = new RenameFileSearchRequestor(this);
// Update any dot delimited changes
final String oldName = this.currentProject.getName();
final String newName = getArguments().getNewName();
final String changeId = newName;
Map<FileTextSearchScope, Pattern> namePatternMap = createNamePatternMap();
for (Entry<FileTextSearchScope, Pattern> namePattern : namePatternMap.entrySet()) {
searchRequestor.createChange(namePattern.getKey(), changeId, namePattern.getValue(), newName, oldName, pm);
}
// Update any path changes
final String oldPath = oldName.replace(".", "/");
final String newPath = newName.replace(".", "/");
Map<FileTextSearchScope, Pattern> pathPatternMap = createPathPatternMap();
for (Entry<FileTextSearchScope, Pattern> pathPattern : pathPatternMap.entrySet()) {
searchRequestor.createChange(pathPattern.getKey(), changeId, pathPattern.getValue(), newPath, oldPath, pm);
}
// If no changes were recorded, exit the function
if (searchRequestor.getChanges().isEmpty()) {
return null;
}
// Collect a composite of all TextFileChanges created by the SearchRequestor
final CompositeChange result = new CompositeChange("");
for (final Iterator<TextFileChange> iter = searchRequestor.getChanges().values().iterator(); iter.hasNext();) {
result.add(iter.next());
}
// Update file name for *.spec
for (final IFile file : searchRequestor.getAffectedFiles()) {
if (file.getFullPath().lastSegment().endsWith(".spec") || file.getFullPath().lastSegment().endsWith(".pc.in")) {
String fileName = file.getFullPath().lastSegment().replace(oldName, newName);
RenameResourceChange change = new RenameResourceChange(file.getFullPath(), fileName);
result.add(change);
}
}
// Handle updating Java packages, if necessary
if (this.currentProject.isNatureEnabled(JavaCore.NATURE_ID)) {
IJavaProject javaProject = JavaCore.create(this.currentProject);
IPackageFragment[] packages = javaProject.getPackageFragments();
for (IPackageFragment pkg : packages) {
if (pkg.getKind() == IPackageFragmentRoot.K_SOURCE) {
if (pkg.getElementName().equals(oldName + ".java")) {
RenamePackageChange packageChange = new RenamePackageChange(pkg, newName + ".java", true);
result.add(packageChange);
}
}
}
}
return result;
}
/**
* A map used to determine files where we need to update dot(.) delimited patterns (e.g. replace Foo.Bar with
* Foo.Baz.Bar).
*/
protected Map<FileTextSearchScope, Pattern> createNamePatternMap() {
Map<FileTextSearchScope, Pattern> namePatternMap = new HashMap<FileTextSearchScope, Pattern>();
final IResource[] roots = { this.currentProject };
final String oldProjectName = this.currentProject.getName();
/* A Map where:
* - Key is the name of the files we want to look in
* - Value is the pattern we are using to find the text we want to replace
*/
Map<String[], String> filePatterns = new HashMap<String[], String>();
filePatterns.put(new String[] { "*" + SpdPackage.FILE_EXTENSION }, oldProjectName + "(?:.*type=)");
filePatterns.put(new String[] { "*.spec", "*.pc.in" }, "(?:Name:\\s*)" + oldProjectName);
filePatterns.put(new String[] { "*.java" }, "(?:package.*)" + oldProjectName);
filePatterns.put(new String[] { "*.wavedev" }, "(?:properties.*)" + oldProjectName);
filePatterns.put(new String[] { "build.sh" }, "(?:-e.*)" + oldProjectName + "|(?:tmpdir.*)");
filePatterns.put(new String[] { "configure.ac" }, "(?:AC_INIT.*)" + oldProjectName);
filePatterns.put(new String[] { "configure.ac" }, "(?:RH_SOFTPKG_PREFIX.*)" + oldProjectName);
filePatterns.put(new String[] { "configure.ac" }, "(?:AC_CONFIG_FILES.*)" + oldProjectName);
filePatterns.put(new String[] { "Makefile.am" }, "(?:ossieName.*)" + oldProjectName);
filePatterns.put(new String[] { "Makefile.am" }, "(?:pkgconfig_DATA.*)" + oldProjectName);
filePatterns.put(new String[] { "startJava.sh" }, "(?<!\\.)" + oldProjectName + "(?!\\.jar)");
for (Entry<String[], String> filePattern : filePatterns.entrySet()) {
FileTextSearchScope scope = FileTextSearchScope.newSearchScope(roots, filePattern.getKey(), true);
Pattern pattern = Pattern.compile(filePattern.getValue());
namePatternMap.put(scope, pattern);
}
return namePatternMap;
}
/**
* A map used to determine files where we need to update forward-slash(/) delimited patterns (e.g. replace Foo/Bar
* with Foo/Baz/Bar)
*/
private Map<FileTextSearchScope, Pattern> createPathPatternMap() {
Map<FileTextSearchScope, Pattern> pathPatternMap = new HashMap<FileTextSearchScope, Pattern>();
final IResource[] roots = { this.currentProject };
final String oldProjectPath = this.currentProject.getName().replace(".", "/");
/* A Map where:
* - Key is the name of the files we want to look in
* - Value is the pattern we are using to find the text we want to replace
*/
Map<String[], String> filePatterns = new HashMap<String[], String>();
filePatterns.put(new String[] { "Makefile.am" }, "(?:\\$\\(prefix\\).*)" + oldProjectPath);
// Update all paths in the .spec file EXCEPT those describing %dir
filePatterns.put(new String[] { "*.spec" }, "(?<!\\%dir )(?:\\%\\{_prefix\\}.*)" + oldProjectPath + "(?!\\.)");
filePatterns.put(new String[] { "*.spec" }, "(?<!\\%dir )(?:\\%\\{_sdrroot\\}.*)" + oldProjectPath + "(?!\\.)");
// Update the %dir declarations in .spec files. These are special cases that need to be handled
filePatterns.put(new String[] { "*.spec" }, "(?s:\\%dir \\%\\{_prefix\\}.*?)" + oldProjectPath);
filePatterns.put(new String[] { "*.spec" }, "(?s:\\%dir \\%\\{_sdrroot\\}.*?)" + oldProjectPath);
for (Entry<String[], String> filePattern : filePatterns.entrySet()) {
FileTextSearchScope scope = FileTextSearchScope.newSearchScope(roots, filePattern.getKey(), true);
Pattern pattern = Pattern.compile(filePattern.getValue());
pathPatternMap.put(scope, pattern);
}
return pathPatternMap;
}
@Override
public Change createChange(final IProgressMonitor pm) throws CoreException {
// Refresh resource after changes have been made
this.currentProject.refreshLocal(IResource.DEPTH_INFINITE, pm);
return null;
}
@Override
public String getName() {
return "Refactoring associated resources.";
}
/**
* @param currentProject2
* @return True if the project has a nature that is acceptable for this RenameParticipant
* @throws CoreException
*/
protected boolean hasCorrectNature(IProject project) throws CoreException {
return (project.hasNature(ScaComponentProjectNature.ID) && !project.hasNature(ScaNodeProjectNature.ID));
}
@Override
public RefactoringStatus checkConditions(final IProgressMonitor pm, final CheckConditionsContext context) {
// Get project names
String oldProjName = this.currentProject.toString().substring(2); // Trim 'P/' from beginning of project name
String newProjName = this.getArguments().getNewName();
// Get the base names. Add +1 so we don't include the '.', and so we start from index=0 if no '.' is present
String newBaseName = newProjName.substring(newProjName.lastIndexOf('.') + 1);
String oldBaseName = oldProjName.substring(oldProjName.lastIndexOf('.') + 1);
RefactoringStatus status = new RefactoringStatus();
if (!oldBaseName.equals(newBaseName)) {
status.addError("REDHAWK only supports changing project namespaces. Changing the project basename is not supported. "
+ "If you choose to continue please note all references to the original project name must be manually updated");
}
return status;
}
protected IProject getCurrentProject() {
return currentProject;
}
}