aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java547
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java362
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java529
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java148
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java224
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java177
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java752
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java211
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java157
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java410
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java163
11 files changed, 3680 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java
new file mode 100644
index 000000000..b821777a5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java
@@ -0,0 +1,547 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_PACKAGE;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.FileStatusContext;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.RefactoringStatusContext;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
+import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
+import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A participant to participate in refactorings that rename a package in an Android project.
+ * The class updates android manifest and the layout file
+ * The user can suppress refactoring by disabling the "Update references" checkbox
+ * <p>
+ * Rename participants are registered via the extension point <code>
+ * org.eclipse.ltk.core.refactoring.renameParticipants</code>.
+ * Extensions to this extension point must therefore extend
+ * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
+ * </p>
+ */
+@SuppressWarnings("restriction")
+public class AndroidPackageRenameParticipant extends RenameParticipant {
+
+ private IProject mProject;
+ private IFile mManifestFile;
+ private IPackageFragment mPackageFragment;
+ private String mOldPackage;
+ private String mNewPackage;
+ private String mAppPackage;
+ private boolean mRefactoringAppPackage;
+
+ @Override
+ public String getName() {
+ return "Android Package Rename";
+ }
+
+ @Override
+ public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
+ throws OperationCanceledException {
+ if (mAppPackage.equals(mOldPackage) && !mRefactoringAppPackage) {
+ IRegion region = null;
+ Document document = DomUtilities.getDocument(mManifestFile);
+ if (document != null && document.getDocumentElement() != null) {
+ Attr attribute = document.getDocumentElement().getAttributeNode(ATTR_PACKAGE);
+ if (attribute instanceof IndexedRegion) {
+ IndexedRegion ir = (IndexedRegion) attribute;
+ int start = ir.getStartOffset();
+ region = new Region(start, ir.getEndOffset() - start);
+ }
+ }
+ if (region == null) {
+ region = new Region(0, 0);
+ }
+ // There's no line wrapping in the error dialog, so split up the message into
+ // individually digestible pieces of information
+ RefactoringStatusContext ctx = new FileStatusContext(mManifestFile, region);
+ RefactoringStatus status = RefactoringStatus.createInfoStatus(
+ "You are refactoring the same package as your application's " +
+ "package (specified in the manifest).\n", ctx);
+ status.addInfo(
+ "Note that this refactoring does NOT also update your " +
+ "application package.", ctx);
+ status.addInfo("The application package defines your application's identity.", ctx);
+ status.addInfo(
+ "If you change it, then it is considered to be a different application.", ctx);
+ status.addInfo("(Users of the previous version cannot update to the new version.)",
+ ctx);
+ status.addInfo(
+ "The application package, and the package containing the code, can differ.",
+ ctx);
+ status.addInfo(
+ "To really change application package, " +
+ "choose \"Android Tools\" > \"Rename Application Package.\" " +
+ "from the project context menu.", ctx);
+ return status;
+ }
+
+ return new RefactoringStatus();
+ }
+
+ @Override
+ protected boolean initialize(final Object element) {
+ mRefactoringAppPackage = false;
+ try {
+ // Only propose this refactoring if the "Update References" checkbox is set.
+ if (!getArguments().getUpdateReferences()) {
+ return false;
+ }
+
+ if (element instanceof IPackageFragment) {
+ mPackageFragment = (IPackageFragment) element;
+ if (!mPackageFragment.containsJavaResources()) {
+ return false;
+ }
+ IJavaProject javaProject = (IJavaProject) mPackageFragment
+ .getAncestor(IJavaElement.JAVA_PROJECT);
+ mProject = javaProject.getProject();
+ IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
+ + SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ if (manifestResource == null || !manifestResource.exists()
+ || !(manifestResource instanceof IFile)) {
+ RefactoringUtil.logInfo("Invalid or missing the "
+ + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the "
+ + mProject.getName() + " project.");
+ return false;
+ }
+ mManifestFile = (IFile) manifestResource;
+ String packageName = mPackageFragment.getElementName();
+ ManifestData manifestData;
+ manifestData = AndroidManifestHelper.parseForData(mManifestFile);
+ if (manifestData == null) {
+ return false;
+ }
+ mAppPackage = manifestData.getPackage();
+ mOldPackage = packageName;
+ mNewPackage = getArguments().getNewName();
+ if (mOldPackage == null || mNewPackage == null) {
+ return false;
+ }
+
+ if (RefactoringUtil.isRefactorAppPackage()
+ && mAppPackage != null
+ && mAppPackage.equals(packageName)) {
+ mRefactoringAppPackage = true;
+ }
+
+ return true;
+ }
+ } catch (JavaModelException ignore) {
+ }
+ return false;
+ }
+
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ if (pm.isCanceled()) {
+ return null;
+ }
+ if (!getArguments().getUpdateReferences()) {
+ return null;
+ }
+
+ RefactoringProcessor p = getProcessor();
+ if (p instanceof RenameCompilationUnitProcessor) {
+ RenameTypeProcessor rtp =
+ ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
+ if (rtp != null) {
+ String pattern = rtp.getFilePatterns();
+ boolean updQualf = rtp.getUpdateQualifiedNames();
+ if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
+ // Do not propose this refactoring if the
+ // "Update fully qualified names in non-Java files" option is
+ // checked and the file patterns mention XML. [c.f. SDK bug 21589]
+ return null;
+ }
+ }
+ }
+
+ IPath pkgPath = mPackageFragment.getPath();
+ IPath genPath = mProject.getFullPath().append(SdkConstants.FD_GEN_SOURCES);
+ if (genPath.isPrefixOf(pkgPath)) {
+ RefactoringUtil.logInfo(getName() + ": Cannot rename generated package.");
+ return null;
+ }
+ CompositeChange result = new CompositeChange(getName());
+ result.markAsSynthetic();
+
+ addManifestFileChanges(result);
+
+ // Update layout files; we don't just need to react to custom view
+ // changes, we need to update fragment references and even tool:context activity
+ // references
+ addLayoutFileChanges(mProject, result);
+
+ // Also update in dependent projects
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ if (projectState != null) {
+ Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
+ for (ProjectState parentProject : parentProjects) {
+ IProject project = parentProject.getProject();
+ addLayoutFileChanges(project, result);
+ }
+ }
+
+ if (mRefactoringAppPackage) {
+ Change genChange = getGenPackageChange(pm);
+ if (genChange != null) {
+ result.add(genChange);
+ }
+
+ return new NullChange("Update Imports") {
+ @Override
+ public Change perform(IProgressMonitor monitor) throws CoreException {
+ FixImportsJob job = new FixImportsJob("Fix Rename Package",
+ mManifestFile, mNewPackage);
+ job.schedule(500);
+
+ // Not undoable: just return null instead of an undo-change.
+ return null;
+ }
+ };
+ }
+
+ return (result.getChildren().length == 0) ? null : result;
+ }
+
+ /**
+ * Returns Android gen package text change
+ *
+ * @param pm the progress monitor
+ *
+ * @return Android gen package text change
+ * @throws CoreException if an error happens
+ * @throws OperationCanceledException if the operation is canceled
+ */
+ public Change getGenPackageChange(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ if (mRefactoringAppPackage) {
+ IPackageFragment genJavaPackageFragment = getGenPackageFragment();
+ if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) {
+ return new RenamePackageChange(genJavaPackageFragment, mNewPackage, true);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the gen package fragment
+ */
+ private IPackageFragment getGenPackageFragment() throws JavaModelException {
+ IJavaProject javaProject = (IJavaProject) mPackageFragment
+ .getAncestor(IJavaElement.JAVA_PROJECT);
+ if (javaProject != null && javaProject.isOpen()) {
+ IProject project = javaProject.getProject();
+ IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES);
+ if (genFolder.exists()) {
+ String javaPackagePath = mAppPackage.replace('.', '/');
+ IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath);
+ IPackageFragment genPackageFragment = javaProject
+ .findPackageFragment(genJavaPackagePath);
+ return genPackageFragment;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the new class name
+ *
+ * @param fqcn the fully qualified class name in the renamed package
+ * @return the new class name
+ */
+ private String getNewClassName(String fqcn) {
+ assert isInRenamedPackage(fqcn) : fqcn;
+ int lastDot = fqcn.lastIndexOf('.');
+ if (lastDot < 0) {
+ return mNewPackage;
+ }
+ String name = fqcn.substring(lastDot, fqcn.length());
+ String newClassName = mNewPackage + name;
+ return newClassName;
+ }
+
+ private void addManifestFileChanges(CompositeChange result) {
+ addXmlFileChanges(mManifestFile, result, true);
+ }
+
+ private void addLayoutFileChanges(IProject project, CompositeChange result) {
+ try {
+ // Update references in XML resource files
+ IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ IResource[] folders = resFolder.members();
+ for (IResource folder : folders) {
+ String folderName = folder.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
+ if (folderType != ResourceFolderType.LAYOUT) {
+ continue;
+ }
+ if (!(folder instanceof IFolder)) {
+ continue;
+ }
+ IResource[] files = ((IFolder) folder).members();
+ for (int i = 0; i < files.length; i++) {
+ IResource member = files[i];
+ if ((member instanceof IFile) && member.exists()) {
+ IFile file = (IFile) member;
+ String fileName = member.getName();
+
+ if (SdkUtils.endsWith(fileName, DOT_XML)) {
+ addXmlFileChanges(file, result, false);
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ RefactoringUtil.log(e);
+ }
+ }
+
+ private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ IStructuredDocument document = model.getStructuredDocument();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Element root = domModel.getDocument().getDocumentElement();
+ if (root != null) {
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+ if (isManifest) {
+ addManifestReplacements(edits, root, document);
+ } else {
+ addLayoutReplacements(edits, root, document);
+ }
+ if (!edits.isEmpty()) {
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ change.setTextType(EXT_XML);
+ change.setEdit(rootEdit);
+ changes.add(change);
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isInRenamedPackage(String fqcn) {
+ return fqcn.startsWith(mOldPackage)
+ && fqcn.length() > mOldPackage.length()
+ && fqcn.indexOf('.', mOldPackage.length() + 1) == -1;
+ }
+
+ private void addLayoutReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ String tag = element.getTagName();
+ if (isInRenamedPackage(tag)) {
+ int start = RefactoringUtil.getTagNameRangeStart(element, document);
+ if (start != -1) {
+ int end = start + tag.length();
+ edits.add(new ReplaceEdit(start, end - start, getNewClassName(tag)));
+ }
+ } else {
+ Attr classNode = null;
+ if (tag.equals(VIEW_TAG)) {
+ classNode = element.getAttributeNode(ATTR_CLASS);
+ } else if (tag.equals(VIEW_FRAGMENT)) {
+ classNode = element.getAttributeNode(ATTR_CLASS);
+ if (classNode == null) {
+ classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
+ }
+ } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
+ classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
+ if (classNode != null && classNode.getValue().startsWith(".")) { //$NON-NLS-1$
+ classNode = null;
+ }
+ }
+ if (classNode != null) {
+ String fqcn = classNode.getValue();
+ if (isInRenamedPackage(fqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + fqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, getNewClassName(fqcn)));
+ }
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addLayoutReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+
+ private void addManifestReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ if (mRefactoringAppPackage &&
+ element == element.getOwnerDocument().getDocumentElement()) {
+ // Update the app package declaration
+ Attr pkg = element.getAttributeNode(ATTR_PACKAGE);
+ if (pkg != null && pkg.getValue().equals(mOldPackage)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(pkg, document);
+ if (start != -1) {
+ int end = start + mOldPackage.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewPackage));
+ }
+ }
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ if (!RefactoringUtil.isManifestClassAttribute(attr)) {
+ continue;
+ }
+
+ String value = attr.getValue();
+ if (isInRenamedPackage(value)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + value.length();
+ edits.add(new ReplaceEdit(start, end - start, getNewClassName(value)));
+ }
+ } else if (value.startsWith(".")) {
+ // If we're renaming the app package
+ String fqcn = mAppPackage + value;
+ if (isInRenamedPackage(fqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + value.length();
+ String newClassName = getNewClassName(fqcn);
+ if (mRefactoringAppPackage) {
+ newClassName = newClassName.substring(mNewPackage.length());
+ } else if (newClassName.startsWith(mOldPackage)
+ && newClassName.charAt(mOldPackage.length()) == '.') {
+ newClassName = newClassName.substring(mOldPackage.length());
+ }
+
+ if (!newClassName.equals(value)) {
+ edits.add(new ReplaceEdit(start, end - start, newClassName));
+ }
+ }
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addManifestReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java
new file mode 100644
index 000000000..2146184c8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+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.OperationCanceledException;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+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.MoveParticipant;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A participant to participate in refactorings that move a type in an Android project.
+ * The class updates android manifest and the layout file
+ * The user can suppress refactoring by disabling the "Update references" checkbox
+ * <p>
+ * Rename participants are registered via the extension point <code>
+ * org.eclipse.ltk.core.refactoring.moveParticipants</code>.
+ * Extensions to this extension point must therefore extend <code>org.eclipse.ltk.core.refactoring.participants.MoveParticipant</code>.
+ * </p>
+ */
+@SuppressWarnings("restriction")
+public class AndroidTypeMoveParticipant extends MoveParticipant {
+
+ private IProject mProject;
+ protected IFile mManifestFile;
+ protected String mOldFqcn;
+ protected String mNewFqcn;
+ protected String mAppPackage;
+
+ @Override
+ public String getName() {
+ return "Android Type Move";
+ }
+
+ @Override
+ public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
+ throws OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ protected boolean initialize(Object element) {
+ if (element instanceof IType) {
+ IType type = (IType) element;
+ IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT);
+ mProject = javaProject.getProject();
+ IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
+ + SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ if (manifestResource == null || !manifestResource.exists()
+ || !(manifestResource instanceof IFile)) {
+ RefactoringUtil.logInfo("Invalid or missing the "
+ + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + mProject.getName()
+ + " project.");
+ return false;
+ }
+ mManifestFile = (IFile) manifestResource;
+ ManifestData manifestData;
+ manifestData = AndroidManifestHelper.parseForData(mManifestFile);
+ if (manifestData == null) {
+ return false;
+ }
+ mAppPackage = manifestData.getPackage();
+ mOldFqcn = type.getFullyQualifiedName();
+ Object destination = getArguments().getDestination();
+ if (destination instanceof IPackageFragment) {
+ IPackageFragment packageFragment = (IPackageFragment) destination;
+ mNewFqcn = packageFragment.getElementName() + "." + type.getElementName();
+ } else if (destination instanceof IResource) {
+ try {
+ IPackageFragment[] fragments = javaProject.getPackageFragments();
+ for (IPackageFragment fragment : fragments) {
+ IResource resource = fragment.getResource();
+ if (resource.equals(destination)) {
+ mNewFqcn = fragment.getElementName() + '.' + type.getElementName();
+ break;
+ }
+ }
+ } catch (JavaModelException e) {
+ // pass
+ }
+ }
+ return mOldFqcn != null && mNewFqcn != null;
+ }
+
+ return false;
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ if (pm.isCanceled()) {
+ return null;
+ }
+ if (!getArguments().getUpdateReferences()) {
+ return null;
+ }
+ CompositeChange result = new CompositeChange(getName());
+ result.markAsSynthetic();
+
+ addManifestFileChanges(result);
+
+ // Update layout files; we don't just need to react to custom view
+ // changes, we need to update fragment references and even tool:context activity
+ // references
+ addLayoutFileChanges(mProject, result);
+
+ // Also update in dependent projects
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ if (projectState != null) {
+ Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
+ for (ProjectState parentProject : parentProjects) {
+ IProject project = parentProject.getProject();
+ addLayoutFileChanges(project, result);
+ }
+ }
+
+ return (result.getChildren().length == 0) ? null : result;
+ }
+
+ private void addManifestFileChanges(CompositeChange result) {
+ addXmlFileChanges(mManifestFile, result, true);
+ }
+
+ private void addLayoutFileChanges(IProject project, CompositeChange result) {
+ try {
+ // Update references in XML resource files
+ IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ IResource[] folders = resFolder.members();
+ for (IResource folder : folders) {
+ String folderName = folder.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
+ if (folderType != ResourceFolderType.LAYOUT) {
+ continue;
+ }
+ if (!(folder instanceof IFolder)) {
+ continue;
+ }
+ IResource[] files = ((IFolder) folder).members();
+ for (int i = 0; i < files.length; i++) {
+ IResource member = files[i];
+ if ((member instanceof IFile) && member.exists()) {
+ IFile file = (IFile) member;
+ String fileName = member.getName();
+
+ if (SdkUtils.endsWith(fileName, DOT_XML)) {
+ addXmlFileChanges(file, result, false);
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ RefactoringUtil.log(e);
+ }
+ }
+
+ private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ IStructuredDocument document = model.getStructuredDocument();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Element root = domModel.getDocument().getDocumentElement();
+ if (root != null) {
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+ if (isManifest) {
+ addManifestReplacements(edits, root, document);
+ } else {
+ addLayoutReplacements(edits, root, document);
+ }
+ if (!edits.isEmpty()) {
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ change.setTextType(EXT_XML);
+ change.setEdit(rootEdit);
+ changes.add(change);
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return false;
+ }
+
+ private void addLayoutReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ String tag = element.getTagName();
+ if (tag.equals(mOldFqcn)) {
+ int start = RefactoringUtil.getTagNameRangeStart(element, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ } else if (tag.equals(VIEW_TAG)) {
+ Attr classNode = element.getAttributeNode(ATTR_CLASS);
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ } else if (tag.equals(VIEW_FRAGMENT)) {
+ Attr classNode = element.getAttributeNode(ATTR_CLASS);
+ if (classNode == null) {
+ classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
+ }
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
+ Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addLayoutReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+
+ private void addManifestReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ if (!RefactoringUtil.isManifestClassAttribute(attr)) {
+ continue;
+ }
+
+ String value = attr.getValue();
+ if (value.equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ } else if (value.startsWith(".")) { //$NON-NLS-1$
+ String fqcn = mAppPackage + value;
+ if (fqcn.equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + value.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addManifestReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java
new file mode 100644
index 000000000..7843ab3b4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_CONTEXT;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.CLASS_VIEW;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.R_CLASS;
+import static com.android.SdkConstants.TOOLS_URI;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+import static com.android.SdkConstants.VIEW_TAG;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+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.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jdt.core.IField;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
+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.RefactoringProcessor;
+import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
+import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A participant to participate in refactorings that rename a type in an Android project.
+ * The class updates android manifest and the layout file
+ * The user can suppress refactoring by disabling the "Update references" checkbox.
+ * <p>
+ * Rename participants are registered via the extension point <code>
+ * org.eclipse.ltk.core.refactoring.renameParticipants</code>.
+ * Extensions to this extension point must therefore extend
+ * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
+ */
+@SuppressWarnings("restriction")
+public class AndroidTypeRenameParticipant extends RenameParticipant {
+ private IProject mProject;
+ private IFile mManifestFile;
+ private String mOldFqcn;
+ private String mNewFqcn;
+ private String mOldSimpleName;
+ private String mNewSimpleName;
+ private String mOldDottedName;
+ private String mNewDottedName;
+ private boolean mIsCustomView;
+
+ /**
+ * Set while we are creating an embedded Java refactoring. This could cause a recursive
+ * invocation of the XML renaming refactoring to react to the field, so this is flag
+ * during the call to the Java processor, and is used to ignore requests for adding in
+ * field reactions during that time.
+ */
+ private static boolean sIgnore;
+
+ @Override
+ public String getName() {
+ return "Android Type Rename";
+ }
+
+ @Override
+ public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
+ throws OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ protected boolean initialize(Object element) {
+ if (sIgnore) {
+ return false;
+ }
+
+ if (element instanceof IType) {
+ IType type = (IType) element;
+ IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT);
+ mProject = javaProject.getProject();
+ IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
+ + SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ if (manifestResource == null || !manifestResource.exists()
+ || !(manifestResource instanceof IFile)) {
+ RefactoringUtil.logInfo(
+ String.format("Invalid or missing file %1$s in project %2$s",
+ SdkConstants.FN_ANDROID_MANIFEST_XML,
+ mProject.getName()));
+ return false;
+ }
+
+ try {
+ IType classView = javaProject.findType(CLASS_VIEW);
+ if (classView != null) {
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
+ if (hierarchy.contains(classView)) {
+ mIsCustomView = true;
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ mManifestFile = (IFile) manifestResource;
+ ManifestData manifestData;
+ manifestData = AndroidManifestHelper.parseForData(mManifestFile);
+ if (manifestData == null) {
+ return false;
+ }
+ mOldSimpleName = type.getElementName();
+ mOldDottedName = '.' + mOldSimpleName;
+ mOldFqcn = type.getFullyQualifiedName();
+ String packageName = type.getPackageFragment().getElementName();
+ mNewSimpleName = getArguments().getNewName();
+ mNewDottedName = '.' + mNewSimpleName;
+ if (packageName != null) {
+ mNewFqcn = packageName + mNewDottedName;
+ } else {
+ mNewFqcn = mNewSimpleName;
+ }
+ if (mOldFqcn == null || mNewFqcn == null) {
+ return false;
+ }
+ if (!RefactoringUtil.isRefactorAppPackage() && mNewFqcn.indexOf('.') == -1) {
+ mNewFqcn = packageName + mNewDottedName;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ if (pm.isCanceled()) {
+ return null;
+ }
+
+ // Only propose this refactoring if the "Update References" checkbox is set.
+ if (!getArguments().getUpdateReferences()) {
+ return null;
+ }
+
+ RefactoringProcessor p = getProcessor();
+ if (p instanceof RenameCompilationUnitProcessor) {
+ RenameTypeProcessor rtp =
+ ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
+ if (rtp != null) {
+ String pattern = rtp.getFilePatterns();
+ boolean updQualf = rtp.getUpdateQualifiedNames();
+ if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
+ // Do not propose this refactoring if the
+ // "Update fully qualified names in non-Java files" option is
+ // checked and the file patterns mention XML. [c.f. SDK bug 21589]
+ return null;
+ }
+ }
+ }
+
+ CompositeChange result = new CompositeChange(getName());
+
+ // Only show the children in the refactoring preview dialog
+ result.markAsSynthetic();
+
+ addManifestFileChanges(mManifestFile, result);
+ addLayoutFileChanges(mProject, result);
+ addJavaChanges(mProject, result, pm);
+
+ // Also update in dependent projects
+ // TODO: Also do the Java elements, if they are in Jar files, since the library
+ // projects do this (and the JDT refactoring does not include them)
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ if (projectState != null) {
+ Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
+ for (ProjectState parentProject : parentProjects) {
+ IProject project = parentProject.getProject();
+ IResource manifestResource = project.findMember(AdtConstants.WS_SEP
+ + SdkConstants.FN_ANDROID_MANIFEST_XML);
+ if (manifestResource != null && manifestResource.exists()
+ && manifestResource instanceof IFile) {
+ addManifestFileChanges((IFile) manifestResource, result);
+ }
+ addLayoutFileChanges(project, result);
+ addJavaChanges(project, result, pm);
+ }
+ }
+
+ // Look for the field change on the R.java class; it's a derived file
+ // and will generate file modified manually warnings. Disable it.
+ RenameResourceParticipant.disableRClassChanges(result);
+
+ return (result.getChildren().length == 0) ? null : result;
+ }
+
+ private void addJavaChanges(IProject project, CompositeChange result, IProgressMonitor monitor) {
+ if (!mIsCustomView) {
+ return;
+ }
+
+ // Also rename styleables, if any
+ try {
+ // Find R class
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ ManifestInfo info = ManifestInfo.get(project);
+ info.getPackage();
+ String rFqcn = info.getPackage() + '.' + R_CLASS;
+ IType styleable = javaProject.findType(rFqcn + '.' + ResourceType.STYLEABLE.getName());
+ if (styleable != null) {
+ IField[] fields = styleable.getFields();
+ CompositeChange fieldChanges = null;
+ for (IField field : fields) {
+ String name = field.getElementName();
+ if (name.equals(mOldSimpleName) || name.startsWith(mOldSimpleName)
+ && name.length() > mOldSimpleName.length()
+ && name.charAt(mOldSimpleName.length()) == '_') {
+ // Rename styleable fields
+ String newName = name.equals(mOldSimpleName) ? mNewSimpleName :
+ mNewSimpleName + name.substring(mOldSimpleName.length());
+ RenameRefactoring refactoring =
+ RenameResourceParticipant.createFieldRefactoring(field,
+ newName, true);
+
+ try {
+ sIgnore = true;
+ RefactoringStatus status = refactoring.checkAllConditions(monitor);
+ if (status != null && !status.hasError()) {
+ Change fieldChange = refactoring.createChange(monitor);
+ if (fieldChange != null) {
+ if (fieldChanges == null) {
+ fieldChanges = new CompositeChange(
+ "Update custom view styleable fields");
+ // Disable these changes. They sometimes end up
+ // editing the wrong offsets. It looks like Eclipse
+ // doesn't ensure that after applying each change it
+ // also adjusts the other field offsets. I poked around
+ // and couldn't find a way to do this properly, but
+ // at least by listing the diffs here it shows what should
+ // be done.
+ fieldChanges.setEnabled(false);
+ }
+ // Disable change: see comment above.
+ fieldChange.setEnabled(false);
+ fieldChanges.add(fieldChange);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ sIgnore = false;
+ }
+ }
+ }
+ if (fieldChanges != null) {
+ result.add(fieldChanges);
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ private void addManifestFileChanges(IFile manifestFile, CompositeChange result) {
+ addXmlFileChanges(manifestFile, result, null);
+ }
+
+ private void addLayoutFileChanges(IProject project, CompositeChange result) {
+ try {
+ // Update references in XML resource files
+ IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ IResource[] folders = resFolder.members();
+ for (IResource folder : folders) {
+ String folderName = folder.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
+ if (folderType != ResourceFolderType.LAYOUT &&
+ folderType != ResourceFolderType.VALUES) {
+ continue;
+ }
+ if (!(folder instanceof IFolder)) {
+ continue;
+ }
+ IResource[] files = ((IFolder) folder).members();
+ for (int i = 0; i < files.length; i++) {
+ IResource member = files[i];
+ if ((member instanceof IFile) && member.exists()) {
+ IFile file = (IFile) member;
+ String fileName = member.getName();
+
+ if (SdkUtils.endsWith(fileName, DOT_XML)) {
+ addXmlFileChanges(file, result, folderType);
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ RefactoringUtil.log(e);
+ }
+ }
+
+ private boolean addXmlFileChanges(IFile file, CompositeChange changes,
+ ResourceFolderType folderType) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ IStructuredDocument document = model.getStructuredDocument();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Element root = domModel.getDocument().getDocumentElement();
+ if (root != null) {
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+ if (folderType == null) {
+ assert file.getName().equals(ANDROID_MANIFEST_XML);
+ addManifestReplacements(edits, root, document);
+ } else if (folderType == ResourceFolderType.VALUES) {
+ addValueReplacements(edits, root, document);
+ } else {
+ assert folderType == ResourceFolderType.LAYOUT;
+ addLayoutReplacements(edits, root, document);
+ }
+ if (!edits.isEmpty()) {
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ change.setTextType(EXT_XML);
+ change.setEdit(rootEdit);
+ changes.add(change);
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return false;
+ }
+
+ private void addLayoutReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ String tag = element.getTagName();
+ if (tag.equals(mOldFqcn)) {
+ int start = RefactoringUtil.getTagNameRangeStart(element, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ } else if (tag.equals(VIEW_TAG)) {
+ // TODO: Handle inner classes ($ vs .) ?
+ Attr classNode = element.getAttributeNode(ATTR_CLASS);
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ } else if (tag.equals(VIEW_FRAGMENT)) {
+ Attr classNode = element.getAttributeNode(ATTR_CLASS);
+ if (classNode == null) {
+ classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
+ }
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ }
+ } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
+ Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
+ if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ } else if (classNode != null && classNode.getValue().equals(mOldDottedName)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
+ if (start != -1) {
+ int end = start + mOldDottedName.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addLayoutReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+
+ private void addValueReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element root,
+ @NonNull IStructuredDocument document) {
+ // Look for styleable renames for custom views
+ String declareStyleable = ResourceType.DECLARE_STYLEABLE.getName();
+ List<Element> topLevel = DomUtilities.getChildren(root);
+ for (Element element : topLevel) {
+ String tag = element.getTagName();
+ if (declareStyleable.equals(tag)) {
+ Attr nameNode = element.getAttributeNode(ATTR_NAME);
+ if (nameNode != null && mOldSimpleName.equals(nameNode.getValue())) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document);
+ if (start != -1) {
+ int end = start + mOldSimpleName.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewSimpleName));
+ }
+ }
+ }
+ }
+ }
+
+ private void addManifestReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document) {
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ if (!RefactoringUtil.isManifestClassAttribute(attr)) {
+ continue;
+ }
+
+ String value = attr.getValue();
+ if (value.equals(mOldFqcn)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + mOldFqcn.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
+ }
+ } else if (value.equals(mOldDottedName)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + mOldDottedName.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addManifestReplacements(edits, (Element) child, document);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java
new file mode 100644
index 000000000..552e6a845
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.WorkspaceJob;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.ISourceRange;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.search.TypeNameMatch;
+import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
+import org.eclipse.jdt.internal.corext.codemanipulation.OrganizeImportsOperation;
+import org.eclipse.jdt.internal.corext.codemanipulation.OrganizeImportsOperation.IChooseImportQuery;
+import org.eclipse.jdt.internal.ui.actions.WorkbenchRunnableAdapter;
+import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
+import org.eclipse.jdt.internal.ui.preferences.JavaPreferencesSettings;
+import org.eclipse.jdt.ui.SharedASTProvider;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.progress.IProgressService;
+
+/**
+ * The helper class which fixes the import errors after refactoring
+ *
+ */
+@SuppressWarnings("restriction")
+public class FixImportsJob extends WorkspaceJob {
+
+ private IFile mAndroidManifest;
+
+ private String mJavaPackage;
+
+ /**
+ * Creates a new <code>FixImportsJob</code>
+ *
+ * @param name the job name
+ * @param androidManifest the android manifest file
+ * @param javaPackage the android java package
+ */
+ public FixImportsJob(String name, IFile androidManifest, String javaPackage) {
+ super(name);
+ this.mAndroidManifest = androidManifest;
+ this.mJavaPackage = javaPackage;
+ }
+
+ @Override
+ public IStatus runInWorkspace(final IProgressMonitor monitor) throws CoreException {
+ if (mJavaPackage == null || mAndroidManifest == null || !mAndroidManifest.exists()) {
+ return Status.CANCEL_STATUS;
+ }
+ IProject project = mAndroidManifest.getProject();
+ IJavaProject javaProject = JavaCore.create(project);
+ if (javaProject == null || !javaProject.isOpen()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
+
+ IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
+ for (int i = 0; i < markers.length; i++) {
+ IMarker marker = markers[i];
+ IResource resource = marker.getResource();
+ try {
+ IJavaElement element = JavaCore.create(resource);
+ if (element != null && (element instanceof ICompilationUnit)) {
+ final ICompilationUnit cu = (ICompilationUnit) element;
+ IPackageFragment packageFragment = (IPackageFragment) cu
+ .getAncestor(IJavaElement.PACKAGE_FRAGMENT);
+ if (packageFragment != null && packageFragment.exists()) {
+ String packageName = packageFragment.getElementName();
+ if (packageName != null && packageName.startsWith(mJavaPackage)) {
+ CompilationUnit astRoot = SharedASTProvider.getAST(cu,
+ SharedASTProvider.WAIT_ACTIVE_ONLY, null);
+ CodeGenerationSettings settings = JavaPreferencesSettings
+ .getCodeGenerationSettings(cu.getJavaProject());
+ final boolean hasAmbiguity[] = new boolean[] {
+ false
+ };
+ IChooseImportQuery query = new IChooseImportQuery() {
+ @Override
+ public TypeNameMatch[] chooseImports(TypeNameMatch[][] openChoices,
+ ISourceRange[] ranges) {
+ hasAmbiguity[0] = true;
+ return new TypeNameMatch[0];
+ }
+ };
+ final OrganizeImportsOperation op = new OrganizeImportsOperation(cu,
+ astRoot, settings.importIgnoreLowercase, !cu.isWorkingCopy(),
+ true, query);
+ Display.getDefault().asyncExec(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ IProgressService progressService = PlatformUI
+ .getWorkbench().getProgressService();
+ progressService.run(
+ true,
+ true,
+ new WorkbenchRunnableAdapter(op, op
+ .getScheduleRule()));
+ IEditorPart openEditor = EditorUtility.isOpenInEditor(cu);
+ if (openEditor != null) {
+ openEditor.doSave(monitor);
+ }
+ } catch (Throwable e) {
+ RefactoringUtil.log(e);
+ }
+ }
+ });
+
+ }
+ }
+ }
+ } catch (Throwable e) {
+ RefactoringUtil.log(e);
+ }
+ }
+ return Status.OK_STATUS;
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java
new file mode 100644
index 000000000..04ebcfa26
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_BACKUP_AGENT;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_MANAGE_SPACE_ACTIVITY;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME;
+import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_ACTIVITY;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * The utility class for android refactoring
+ *
+ */
+@SuppressWarnings("restriction")
+public class RefactoringUtil {
+
+ private static boolean sRefactorAppPackage = false;
+
+ /**
+ * Releases SSE read model; saves SSE model if exists edit model
+ * Called in dispose method of refactoring change classes
+ *
+ * @param model the SSE model
+ * @param document the document
+ */
+ public static void fixModel(IStructuredModel model, IDocument document) {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ model = null;
+ if (document == null) {
+ return;
+ }
+ try {
+ model = StructuredModelManager.getModelManager().getExistingModelForEdit(document);
+ if (model != null) {
+ model.save();
+ }
+ } catch (UnsupportedEncodingException e1) {
+ // ignore
+ } catch (IOException e1) {
+ // ignore
+ } catch (CoreException e1) {
+ // ignore
+ } finally {
+ if (model != null) {
+ model.releaseFromEdit();
+ }
+ }
+ }
+
+ /**
+ * Logs the info message
+ *
+ * @param message the message
+ */
+ public static void logInfo(String message) {
+ AdtPlugin.log(IStatus.INFO, AdtPlugin.PLUGIN_ID, message);
+ }
+
+ /**
+ * Logs the the exception
+ *
+ * @param e the exception
+ */
+ public static void log(Throwable e) {
+ AdtPlugin.log(e, e.getMessage());
+ }
+
+ /**
+ * @return true if Rename/Move package needs to change the application package
+ * default is false
+ *
+ */
+ public static boolean isRefactorAppPackage() {
+ return sRefactorAppPackage;
+ }
+
+ /**
+ * @param refactorAppPackage true if Rename/Move package needs to change the application package
+ */
+ public static void setRefactorAppPackage(boolean refactorAppPackage) {
+ RefactoringUtil.sRefactorAppPackage = refactorAppPackage;
+ }
+
+ /**
+ * Returns the range of the attribute value in the given document
+ *
+ * @param attr the attribute to look up
+ * @param document the document containing the attribute
+ * @return the range of the value text, not including quotes, in the document
+ */
+ public static int getAttributeValueRangeStart(
+ @NonNull Attr attr,
+ @NonNull IDocument document) {
+ IndexedRegion region = (IndexedRegion) attr;
+ int potentialStart = attr.getName().length() + 2; // + 2: add ="
+ String text;
+ try {
+ text = document.get(region.getStartOffset(),
+ region.getEndOffset() - region.getStartOffset());
+ } catch (BadLocationException e) {
+ return -1;
+ }
+ String value = attr.getValue();
+ int index = text.indexOf(value, potentialStart);
+ if (index != -1) {
+ return region.getStartOffset() + index;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the start of the tag name of the given element
+ *
+ * @param element the element to look up
+ * @param document the document containing the attribute
+ * @return the index of the start tag in the document
+ */
+ public static int getTagNameRangeStart(
+ @NonNull Element element,
+ @NonNull IDocument document) {
+ IndexedRegion region = (IndexedRegion) element;
+ int potentialStart = 1; // add '<'
+ String text;
+ try {
+ text = document.get(region.getStartOffset(),
+ region.getEndOffset() - region.getStartOffset());
+ } catch (BadLocationException e) {
+ return -1;
+ }
+ int index = text.indexOf(element.getTagName(), potentialStart);
+ if (index != -1) {
+ return region.getStartOffset() + index;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns whether the given manifest attribute should be considered to describe
+ * a class name. These will be eligible for refactoring when classes are renamed
+ * or moved.
+ *
+ * @param attribute the manifest attribute
+ * @return true if this attribute can describe a class
+ */
+ public static boolean isManifestClassAttribute(@NonNull Attr attribute) {
+ return isManifestClassAttribute(
+ attribute.getOwnerElement().getTagName(),
+ attribute.getNamespaceURI(),
+ attribute.getLocalName());
+ }
+
+ /**
+ * Returns whether the given manifest attribute should be considered to describe
+ * a class name. These will be eligible for refactoring when classes are renamed
+ * or moved.
+ *
+ * @param tag the tag, if known
+ * @param uri the attribute namespace, if any
+ * @param name the attribute local name, if any
+ * @return true if this attribute can describe a class
+ */
+ public static boolean isManifestClassAttribute(
+ @Nullable String tag,
+ @Nullable String uri,
+ @Nullable String name) {
+ if (name == null) {
+ return false;
+ }
+
+ if ((name.equals(ATTR_NAME)
+ && (AndroidManifest.NODE_ACTIVITY.equals(tag)
+ || AndroidManifest.NODE_APPLICATION.equals(tag)
+ || AndroidManifest.NODE_INSTRUMENTATION.equals(tag)
+ || AndroidManifest.NODE_PROVIDER.equals(tag)
+ || AndroidManifest.NODE_SERVICE.equals(tag)
+ || AndroidManifest.NODE_RECEIVER.equals(tag)))
+ || name.equals(ATTRIBUTE_TARGET_ACTIVITY)
+ || name.equals(ATTRIBUTE_MANAGE_SPACE_ACTIVITY)
+ || name.equals(ATTRIBUTE_BACKUP_AGENT)
+ || name.equals(ATTRIBUTE_PARENT_ACTIVITY_NAME)) {
+ return ANDROID_URI.equals(uri);
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java
new file mode 100644
index 000000000..6779fd322
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.R_CLASS;
+
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+
+import org.eclipse.jdt.internal.ui.refactoring.TextInputWizardPage;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.Set;
+
+@SuppressWarnings("restriction") // JDT refactoring UI
+class RenameResourcePage extends TextInputWizardPage implements SelectionListener {
+ private Label mXmlLabel;
+ private Label mJavaLabel;
+ private Button mUpdateReferences;
+ private boolean mCanClear;
+ private ResourceType mType;
+ private ResourceNameValidator mValidator;
+
+ /**
+ * Create the wizard.
+ * @param type the type of the resource to be renamed
+ * @param initial initial renamed value
+ * @param canClear whether the dialog should allow clearing the field
+ */
+ public RenameResourcePage(ResourceType type, String initial, boolean canClear) {
+ super(type.getName(), true, initial);
+ mType = type;
+ mCanClear = canClear;
+
+ mValidator = ResourceNameValidator.create(false /*allowXmlExtension*/,
+ (Set<String>) null, mType);
+ }
+
+ @SuppressWarnings("unused") // SWT constructors aren't really unused, they have side effects
+ @Override
+ public void createControl(Composite parent) {
+ Composite container = new Composite(parent, SWT.NULL);
+ setControl(container);
+ initializeDialogUnits(container);
+ container.setLayout(new GridLayout(2, false));
+ Label nameLabel = new Label(container, SWT.NONE);
+ nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ nameLabel.setText("New Name:");
+ Text text = super.createTextInputField(container);
+ text.selectAll();
+ text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ Label xmlLabel = new Label(container, SWT.NONE);
+ xmlLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ xmlLabel.setText("XML:");
+ mXmlLabel = new Label(container, SWT.NONE);
+ mXmlLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ Label javaLabel = new Label(container, SWT.NONE);
+ javaLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ javaLabel.setText("Java:");
+ mJavaLabel = new Label(container, SWT.NONE);
+ mJavaLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1));
+ new Label(container, SWT.NONE);
+ new Label(container, SWT.NONE);
+ mUpdateReferences = new Button(container, SWT.CHECK);
+ mUpdateReferences.setSelection(true);
+ mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
+ mUpdateReferences.setText("Update References");
+ mUpdateReferences.addSelectionListener(this);
+
+ Dialog.applyDialogFont(container);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ RenameResourceProcessor processor = getProcessor();
+ String newName = processor.getNewName();
+ if (newName != null && newName.length() > 0
+ && !newName.equals(getInitialValue())) {
+ Text textField = getTextField();
+ textField.setText(newName);
+ textField.setSelection(0, newName.length());
+ }
+ }
+
+ super.setVisible(visible);
+ }
+
+ @Override
+ protected RefactoringStatus validateTextField(String newName) {
+ if (newName.isEmpty() && isEmptyInputValid()) {
+ getProcessor().setNewName("");
+ return RefactoringStatus.createWarningStatus(
+ "The resource definition will be deleted");
+ }
+
+ String error = mValidator.isValid(newName);
+ if (error != null) {
+ return RefactoringStatus.createErrorStatus(error);
+ }
+
+ RenameResourceProcessor processor = getProcessor();
+ processor.setNewName(newName);
+ return processor.checkNewName(newName);
+ }
+
+ private RenameResourceProcessor getProcessor() {
+ RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
+ return (RenameResourceProcessor) refactoring.getProcessor();
+ }
+
+ @Override
+ protected boolean isEmptyInputValid() {
+ return mCanClear;
+ }
+
+ @Override
+ protected boolean isInitialInputValid() {
+ RenameResourceProcessor processor = getProcessor();
+ return processor.getNewName() != null
+ && !processor.getNewName().equals(processor.getCurrentName());
+ }
+
+ @Override
+ protected void textModified(String text) {
+ super.textModified(text);
+ if (mXmlLabel != null && mJavaLabel != null) {
+ String xml = PREFIX_RESOURCE_REF + mType.getName() + '/' + text;
+ String java = R_CLASS + '.' + mType.getName() + '.' + text;
+ if (text.isEmpty()) {
+ xml = java = "";
+ }
+ mXmlLabel.setText(xml);
+ mJavaLabel.setText(java);
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mUpdateReferences) {
+ RenameResourceProcessor processor = getProcessor();
+ boolean update = mUpdateReferences.getSelection();
+ processor.setUpdateReferences(update);
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java
new file mode 100644
index 000000000..438e82223
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java
@@ -0,0 +1,752 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.DOT_XML;
+import static com.android.SdkConstants.EXT_XML;
+import static com.android.SdkConstants.FD_RES;
+import static com.android.SdkConstants.FN_RESOURCE_CLASS;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+import static com.android.SdkConstants.PREFIX_THEME_REF;
+import static com.android.SdkConstants.R_CLASS;
+import static com.android.SdkConstants.TAG_ITEM;
+import static com.android.SdkConstants.TOOLS_URI;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.utils.SdkUtils;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jdt.core.IField;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor;
+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.TextChange;
+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.participants.RenameRefactoring;
+import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A rename participant handling renames of resources (such as R.id.foo and R.layout.bar).
+ * This reacts to refactorings of fields in the R inner classes (such as R.id), and updates
+ * the XML files as appropriate; renaming .xml files, updating XML attributes, resource
+ * references in style declarations, and so on.
+ */
+@SuppressWarnings("restriction") // WTP API
+public class RenameResourceParticipant extends RenameParticipant {
+ /** The project we're refactoring in */
+ private @NonNull IProject mProject;
+
+ /** The type of the resource we're refactoring, such as {@link ResourceType#ID} */
+ private @NonNull ResourceType mType;
+ /**
+ * The type of the resource folder we're refactoring in, such as
+ * {@link ResourceFolderType#VALUES}. When refactoring non value files, we need to
+ * rename the files as well.
+ */
+ private @NonNull ResourceFolderType mFolderType;
+
+ /** The previous name of the resource */
+ private @NonNull String mOldName;
+
+ /** The new name of the resource */
+ private @NonNull String mNewName;
+
+ /** Whether references to the resource should be updated */
+ private boolean mUpdateReferences;
+
+ /** A match pattern to look for in XML, such as {@code @attr/foo} */
+ private @NonNull String mXmlMatch1;
+
+ /** A match pattern to look for in XML, such as {@code ?attr/foo} */
+ private @Nullable String mXmlMatch2;
+
+ /** A match pattern to look for in XML, such as {@code ?foo} */
+ private @Nullable String mXmlMatch3;
+
+ /** The value to replace a reference to {@link #mXmlMatch1} with, such as {@code @attr/bar} */
+ private @NonNull String mXmlNewValue1;
+
+ /** The value to replace a reference to {@link #mXmlMatch2} with, such as {@code ?attr/bar} */
+ private @Nullable String mXmlNewValue2;
+
+ /** The value to replace a reference to {@link #mXmlMatch3} with, such as {@code ?bar} */
+ private @Nullable String mXmlNewValue3;
+
+ /**
+ * If non null, this refactoring was initiated as a file rename of an XML file (and if
+ * null, we are just reacting to a Java field rename)
+ */
+ private IFile mRenamedFile;
+
+ /**
+ * If renaming a field, we need to create an embedded field refactoring to update the
+ * Java sources referring to the corresponding R class field. This is stored as an
+ * instance such that we can have it participate in both the condition check methods
+ * as well as the {@link #createChange(IProgressMonitor)} refactoring operation.
+ */
+ private RenameRefactoring mFieldRefactoring;
+
+ /**
+ * Set while we are creating an embedded Java refactoring. This could cause a recursive
+ * invocation of the XML renaming refactoring to react to the field, so this is flag
+ * during the call to the Java processor, and is used to ignore requests for adding in
+ * field reactions during that time.
+ */
+ private static boolean sIgnore;
+
+ /**
+ * Creates a new {@linkplain RenameResourceParticipant}
+ */
+ public RenameResourceParticipant() {
+ }
+
+ @Override
+ public String getName() {
+ return "Android Rename Field Participant";
+ }
+
+ @Override
+ protected boolean initialize(Object element) {
+ if (sIgnore) {
+ return false;
+ }
+
+ if (element instanceof IField) {
+ IField field = (IField) element;
+ IType declaringType = field.getDeclaringType();
+ if (declaringType != null) {
+ if (R_CLASS.equals(declaringType.getParent().getElementName())) {
+ String typeName = declaringType.getElementName();
+ mType = ResourceType.getEnum(typeName);
+ if (mType != null) {
+ mUpdateReferences = getArguments().getUpdateReferences();
+ mFolderType = AdtUtils.getFolderTypeFor(mType);
+ IJavaProject javaProject = (IJavaProject) field.getAncestor(
+ IJavaElement.JAVA_PROJECT);
+ mProject = javaProject.getProject();
+ mOldName = field.getElementName();
+ mNewName = getArguments().getNewName();
+ mFieldRefactoring = null;
+ mRenamedFile = null;
+ createXmlSearchPatterns();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ } else if (element instanceof IFile) {
+ IFile file = (IFile) element;
+ mProject = file.getProject();
+ if (BaseProjectHelper.isAndroidProject(mProject)) {
+ IPath path = file.getFullPath();
+ int segments = path.segmentCount();
+ if (segments == 4 && path.segment(1).equals(FD_RES)) {
+ String parentName = file.getParent().getName();
+ mFolderType = ResourceFolderType.getFolderType(parentName);
+ if (mFolderType != null && mFolderType != ResourceFolderType.VALUES) {
+ mType = AdtUtils.getResourceTypeFor(mFolderType);
+ if (mType != null) {
+ mUpdateReferences = getArguments().getUpdateReferences();
+ mProject = file.getProject();
+ mOldName = AdtUtils.stripAllExtensions(file.getName());
+ mNewName = AdtUtils.stripAllExtensions(getArguments().getNewName());
+ mRenamedFile = file;
+ createXmlSearchPatterns();
+
+ mFieldRefactoring = null;
+ IField field = getResourceField(mProject, mType, mOldName);
+ if (field != null) {
+ mFieldRefactoring = createFieldRefactoring(field);
+ } else {
+ // no corresponding field; aapt has not run yet. Perhaps user has
+ // turned off auto build.
+ mFieldRefactoring = null;
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+ } else if (element instanceof String) {
+ String uri = (String) element;
+ if (uri.startsWith(PREFIX_RESOURCE_REF) && !uri.startsWith(ANDROID_PREFIX)) {
+ RenameResourceProcessor processor = (RenameResourceProcessor) getProcessor();
+ mProject = processor.getProject();
+ mType = processor.getType();
+ mFolderType = AdtUtils.getFolderTypeFor(mType);
+ mOldName = processor.getCurrentName();
+ mNewName = processor.getNewName();
+ assert uri.endsWith(mOldName) && uri.contains(mType.getName()) : uri;
+ mUpdateReferences = getArguments().getUpdateReferences();
+ if (mNewName.isEmpty()) {
+ mUpdateReferences = false;
+ }
+ mRenamedFile = null;
+ createXmlSearchPatterns();
+ mFieldRefactoring = null;
+ if (!mNewName.isEmpty()) {
+ IField field = getResourceField(mProject, mType, mOldName);
+ if (field != null) {
+ mFieldRefactoring = createFieldRefactoring(field);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Create nested Java refactoring which updates the R field references, if applicable */
+ private RenameRefactoring createFieldRefactoring(IField field) {
+ return createFieldRefactoring(field, mNewName, mUpdateReferences);
+ }
+
+ /**
+ * Create nested Java refactoring which updates the R field references, if
+ * applicable
+ *
+ * @param field the field to be refactored
+ * @param newName the new name
+ * @param updateReferences whether references should be updated
+ * @return a new rename refactoring
+ */
+ public static RenameRefactoring createFieldRefactoring(
+ @NonNull IField field,
+ @NonNull String newName,
+ boolean updateReferences) {
+ RenameFieldProcessor processor = new RenameFieldProcessor(field);
+ processor.setRenameGetter(false);
+ processor.setRenameSetter(false);
+ RenameRefactoring refactoring = new RenameRefactoring(processor);
+ processor.setUpdateReferences(updateReferences);
+ processor.setUpdateTextualMatches(false);
+ processor.setNewElementName(newName);
+ try {
+ if (refactoring.isApplicable()) {
+ return refactoring;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ private void createXmlSearchPatterns() {
+ // Set up search strings for the attribute iterator. This will
+ // identify string matches for mXmlMatch1, 2 and 3, and when matched,
+ // will add a replacement edit for mXmlNewValue1, 2, or 3.
+ mXmlMatch2 = null;
+ mXmlNewValue2 = null;
+ mXmlMatch3 = null;
+ mXmlNewValue3 = null;
+
+ String typeName = mType.getName();
+ if (mUpdateReferences) {
+ mXmlMatch1 = PREFIX_RESOURCE_REF + typeName + '/' + mOldName;
+ mXmlNewValue1 = PREFIX_RESOURCE_REF + typeName + '/' + mNewName;
+ if (mType == ResourceType.ID) {
+ mXmlMatch2 = NEW_ID_PREFIX + mOldName;
+ mXmlNewValue2 = NEW_ID_PREFIX + mNewName;
+ } else if (mType == ResourceType.ATTR) {
+ // When renaming @attr/foo, also edit ?attr/foo
+ mXmlMatch2 = PREFIX_THEME_REF + typeName + '/' + mOldName;
+ mXmlNewValue2 = PREFIX_THEME_REF + typeName + '/' + mNewName;
+ // as well as ?foo
+ mXmlMatch3 = PREFIX_THEME_REF + mOldName;
+ mXmlNewValue3 = PREFIX_THEME_REF + mNewName;
+ }
+ } else if (mType == ResourceType.ID) {
+ mXmlMatch1 = NEW_ID_PREFIX + mOldName;
+ mXmlNewValue1 = NEW_ID_PREFIX + mNewName;
+ }
+ }
+
+ @Override
+ public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
+ throws OperationCanceledException {
+ if (mRenamedFile != null && getArguments().getNewName().indexOf('.') == -1
+ && mRenamedFile.getName().indexOf('.') != -1) {
+ return RefactoringStatus.createErrorStatus(
+ String.format("You must include the file extension (%1$s?)",
+ mRenamedFile.getName().substring(mRenamedFile.getName().indexOf('.'))));
+ }
+
+ // Ensure that the new name is valid
+ if (mNewName != null && !mNewName.isEmpty()) {
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject, mType);
+ String error = validator.isValid(mNewName);
+ if (error != null) {
+ return RefactoringStatus.createErrorStatus(error);
+ }
+ }
+
+ if (mFieldRefactoring != null) {
+ try {
+ sIgnore = true;
+ return mFieldRefactoring.checkAllConditions(pm);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ sIgnore = false;
+ }
+ }
+
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ if (monitor.isCanceled()) {
+ return null;
+ }
+
+ CompositeChange result = new CompositeChange("Update resource references");
+
+ // Only show the children in the refactoring preview dialog
+ result.markAsSynthetic();
+
+ addResourceFileChanges(result, mProject, monitor);
+
+ // If renaming resources in a library project, also offer to rename references
+ // in including projects
+ if (mUpdateReferences) {
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ if (projectState != null && projectState.isLibrary()) {
+ List<ProjectState> parentProjects = projectState.getParentProjects();
+ for (ProjectState state : parentProjects) {
+ IProject project = state.getProject();
+ CompositeChange nested = new CompositeChange(
+ String.format("Update references in %1$s", project.getName()));
+ addResourceFileChanges(nested, project, monitor);
+ if (nested.getChildren().length > 0) {
+ result.add(nested);
+ }
+ }
+ }
+ }
+
+ if (mFieldRefactoring != null) {
+ // We have to add in Java field refactoring
+ try {
+ sIgnore = true;
+ addJavaChanges(result, monitor);
+ } finally {
+ sIgnore = false;
+ }
+ } else {
+ // Disable field refactoring added by the default Java field rename handler
+ disableExistingResourceFileChange();
+ }
+
+ return (result.getChildren().length == 0) ? null : result;
+ }
+
+ /**
+ * Adds all changes to resource files (typically XML but also renaming drawable files
+ *
+ * @param project the Android project
+ * @param className the layout classes
+ */
+ private void addResourceFileChanges(
+ CompositeChange change,
+ IProject project,
+ IProgressMonitor monitor)
+ throws OperationCanceledException {
+ if (monitor.isCanceled()) {
+ return;
+ }
+
+ try {
+ // Update resource references in the manifest
+ IFile manifest = project.getFile(SdkConstants.ANDROID_MANIFEST_XML);
+ if (manifest != null) {
+ addResourceXmlChanges(manifest, change, null);
+ }
+
+ // Update references in XML resource files
+ IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ IResource[] folders = resFolder.members();
+ for (IResource folder : folders) {
+ if (!(folder instanceof IFolder)) {
+ continue;
+ }
+ String folderName = folder.getName();
+ ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
+ IResource[] files = ((IFolder) folder).members();
+ for (int i = 0; i < files.length; i++) {
+ IResource member = files[i];
+ if ((member instanceof IFile) && member.exists()) {
+ IFile file = (IFile) member;
+ String fileName = member.getName();
+
+ if (SdkUtils.endsWith(fileName, DOT_XML)) {
+ addResourceXmlChanges(file, change, folderType);
+ }
+
+ if ((mRenamedFile == null || !mRenamedFile.equals(file))
+ && fileName.startsWith(mOldName)
+ && fileName.length() > mOldName.length()
+ && fileName.charAt(mOldName.length()) == '.'
+ && mFolderType != ResourceFolderType.VALUES
+ && mFolderType == folderType) {
+ // Rename this file
+ String newFile = mNewName + fileName.substring(mOldName.length());
+ IPath path = file.getFullPath();
+ change.add(new RenameResourceChange(path, newFile));
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ RefactoringUtil.log(e);
+ }
+ }
+
+ private void addJavaChanges(CompositeChange result, IProgressMonitor monitor)
+ throws CoreException, OperationCanceledException {
+ if (monitor.isCanceled()) {
+ return;
+ }
+
+ RefactoringStatus status = mFieldRefactoring.checkAllConditions(monitor);
+ if (status != null && !status.hasError()) {
+ Change fieldChanges = mFieldRefactoring.createChange(monitor);
+ if (fieldChanges != null) {
+ result.add(fieldChanges);
+
+ // Look for the field change on the R.java class; it's a derived file
+ // and will generate file modified manually warnings. Disable it.
+ disableRClassChanges(fieldChanges);
+ }
+ }
+ }
+
+ private boolean addResourceXmlChanges(
+ IFile file,
+ CompositeChange changes,
+ ResourceFolderType folderType) {
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ IStructuredModel model = null;
+ try {
+ model = modelManager.getExistingModelForRead(file);
+ if (model == null) {
+ model = modelManager.getModelForRead(file);
+ }
+ if (model != null) {
+ IStructuredDocument document = model.getStructuredDocument();
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Element root = domModel.getDocument().getDocumentElement();
+ if (root != null) {
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+ addReplacements(edits, root, document, folderType);
+ if (!edits.isEmpty()) {
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ change.setTextType(EXT_XML);
+ change.setEdit(rootEdit);
+ changes.add(change);
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+ } catch (IOException e) {
+ AdtPlugin.log(e, null);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ } finally {
+ if (model != null) {
+ model.releaseFromRead();
+ }
+ }
+
+ return false;
+ }
+
+ private void addReplacements(
+ @NonNull List<TextEdit> edits,
+ @NonNull Element element,
+ @NonNull IStructuredDocument document,
+ @Nullable ResourceFolderType folderType) {
+ String tag = element.getTagName();
+ if (folderType == ResourceFolderType.VALUES) {
+ // Look for
+ // <item name="main_layout" type="layout">...</item>
+ // <item name="myid" type="id"/>
+ // <string name="mystring">...</string>
+ // etc
+ if (tag.equals(mType.getName())
+ || (tag.equals(TAG_ITEM)
+ && (mType == ResourceType.ID
+ || mType.getName().equals(element.getAttribute(ATTR_TYPE))))) {
+ Attr nameNode = element.getAttributeNode(ATTR_NAME);
+ if (nameNode != null && nameNode.getValue().equals(mOldName)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document);
+ if (start != -1) {
+ int end = start + mOldName.length();
+ edits.add(new ReplaceEdit(start, end - start, mNewName));
+ }
+ }
+ }
+ }
+
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ String value = attr.getValue();
+
+ // If not updating references, only update XML matches that define the id
+ if (!mUpdateReferences && (!ATTR_ID.equals(attr.getLocalName()) ||
+ !ANDROID_URI.equals(attr.getNamespaceURI()))) {
+
+ if (TOOLS_URI.equals(attr.getNamespaceURI()) && value.equals(mXmlMatch1)) {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + mXmlMatch1.length();
+ edits.add(new ReplaceEdit(start, end - start, mXmlNewValue1));
+ }
+ }
+
+ continue;
+ }
+
+ // Replace XML attribute reference, such as
+ // android:id="@+id/oldName" => android:id="+id/newName"
+
+ String match = null;
+ String matchedValue = null;
+
+ if (value.equals(mXmlMatch1)) {
+ match = mXmlMatch1;
+ matchedValue = mXmlNewValue1;
+ } else if (value.equals(mXmlMatch2)) {
+ match = mXmlMatch2;
+ matchedValue = mXmlNewValue2;
+ } else if (value.equals(mXmlMatch3)) {
+ match = mXmlMatch3;
+ matchedValue = mXmlNewValue3;
+ } else {
+ continue;
+ }
+
+ if (match != null) {
+ if (mNewName.isEmpty() && ATTR_ID.equals(attr.getLocalName()) &&
+ ANDROID_URI.equals(attr.getNamespaceURI())) {
+ // Delete attribute
+ IndexedRegion region = (IndexedRegion) attr;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+ edits.add(new ReplaceEdit(start, end - start, ""));
+ } else {
+ int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
+ if (start != -1) {
+ int end = start + match.length();
+ edits.add(new ReplaceEdit(start, end - start, matchedValue));
+ }
+ }
+ }
+ }
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ addReplacements(edits, (Element) child, document, folderType);
+ } else if (child.getNodeType() == Node.TEXT_NODE && mUpdateReferences) {
+ // Replace XML text, such as @color/custom_theme_color in
+ // <item name="android:windowBackground">@color/custom_theme_color</item>
+ //
+ String text = child.getNodeValue();
+ int index = getFirstNonBlankIndex(text);
+ if (index != -1) {
+ String match = null;
+ String matchedValue = null;
+ if (mXmlMatch1 != null
+ && text.startsWith(mXmlMatch1) && text.trim().equals(mXmlMatch1)) {
+ match = mXmlMatch1;
+ matchedValue = mXmlNewValue1;
+ } else if (mXmlMatch2 != null
+ && text.startsWith(mXmlMatch2) && text.trim().equals(mXmlMatch2)) {
+ match = mXmlMatch2;
+ matchedValue = mXmlNewValue2;
+ } else if (mXmlMatch3 != null
+ && text.startsWith(mXmlMatch3) && text.trim().equals(mXmlMatch3)) {
+ match = mXmlMatch3;
+ matchedValue = mXmlNewValue3;
+ }
+ if (match != null) {
+ IndexedRegion region = (IndexedRegion) child;
+ int start = region.getStartOffset() + index;
+ int end = start + match.length();
+ edits.add(new ReplaceEdit(start, end - start, matchedValue));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the index of the first non-space character in the string, or -1
+ * if the string is empty or has only whitespace
+ *
+ * @param s the string to check
+ * @return the index of the first non whitespace character
+ */
+ private int getFirstNonBlankIndex(String s) {
+ for (int i = 0, n = s.length(); i < n; i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Initiates a renaming of a resource item
+ *
+ * @param project the project containing the resource references
+ * @param type the type of resource
+ * @param name the name of the resource
+ * @return false if initiating the rename failed
+ */
+ @Nullable
+ private static IField getResourceField(
+ @NonNull IProject project,
+ @NonNull ResourceType type,
+ @NonNull String name) {
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ if (javaProject == null) {
+ return null;
+ }
+
+ String pkg = ManifestInfo.get(project).getPackage();
+ // TODO: Rename in all libraries too?
+ IType t = javaProject.findType(pkg + '.' + R_CLASS + '.' + type.getName());
+ if (t == null) {
+ return null;
+ }
+
+ return t.getField(name);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Searches for existing changes in the refactoring which modifies the R
+ * field to rename it. it's derived so performing this change will generate
+ * a "generated code was modified manually" warning
+ */
+ private void disableExistingResourceFileChange() {
+ IFolder genFolder = mProject.getFolder(SdkConstants.FD_GEN_SOURCES);
+ if (genFolder != null && genFolder.exists()) {
+ ManifestInfo manifestInfo = ManifestInfo.get(mProject);
+ String pkg = manifestInfo.getPackage();
+ if (pkg != null) {
+ IFile rFile = genFolder.getFile(pkg.replace('.', '/') + '/' + FN_RESOURCE_CLASS);
+ TextChange change = getTextChange(rFile);
+ if (change != null) {
+ change.setEnabled(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Searches for existing changes in the refactoring which modifies the R
+ * field to rename it. it's derived so performing this change will generate
+ * a "generated code was modified manually" warning
+ *
+ * @param change the change to disable R file changes in
+ */
+ public static void disableRClassChanges(Change change) {
+ if (change.getName().equals(FN_RESOURCE_CLASS)) {
+ change.setEnabled(false);
+ }
+ // Look for the field change on the R.java class; it's a derived file
+ // and will generate file modified manually warnings. Disable it.
+ if (change instanceof CompositeChange) {
+ for (Change outer : ((CompositeChange) change).getChildren()) {
+ disableRClassChanges(outer);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java
new file mode 100644
index 000000000..5ea99411c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
+import org.eclipse.ltk.core.refactoring.participants.ParticipantManager;
+import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant;
+import org.eclipse.ltk.core.refactoring.participants.RenameArguments;
+import org.eclipse.ltk.core.refactoring.participants.RenameProcessor;
+import org.eclipse.ltk.core.refactoring.participants.SharableParticipants;
+
+/**
+ * A rename processor for Android resources.
+ */
+public class RenameResourceProcessor extends RenameProcessor {
+ private IProject mProject;
+ private ResourceType mType;
+ private String mCurrentName;
+ private String mNewName;
+ private boolean mUpdateReferences = true;
+ private ResourceNameValidator mValidator;
+ private RenameArguments mRenameArguments;
+
+ /**
+ * Creates a new rename resource processor.
+ *
+ * @param project the project containing the renamed resource
+ * @param type the type of the resource
+ * @param currentName the current name of the resource
+ * @param newName the new name of the resource, or null if not known
+ */
+ public RenameResourceProcessor(
+ @NonNull IProject project,
+ @NonNull ResourceType type,
+ @NonNull String currentName,
+ @Nullable String newName) {
+ mProject = project;
+ mType = type;
+ mCurrentName = currentName;
+ mNewName = newName != null ? newName : currentName;
+ mUpdateReferences= true;
+ mValidator = ResourceNameValidator.create(false, mProject, mType);
+ }
+
+ /**
+ * Returns the project containing the renamed resource
+ *
+ * @return the project containing the renamed resource
+ */
+ @NonNull
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * Returns the new resource name
+ *
+ * @return the new resource name
+ */
+ @NonNull
+ public String getNewName() {
+ return mNewName;
+ }
+
+ /**
+ * Returns the current name of the resource
+ *
+ * @return the current name of the resource
+ */
+ public String getCurrentName() {
+ return mCurrentName;
+ }
+
+ /**
+ * Returns the type of the resource
+ *
+ * @return the type of the resource
+ */
+ @NonNull
+ public ResourceType getType() {
+ return mType;
+ }
+
+ /**
+ * Sets the new name
+ *
+ * @param newName the new name
+ */
+ public void setNewName(@NonNull String newName) {
+ mNewName = newName;
+ }
+
+ /**
+ * Returns {@code true} if the refactoring processor also updates references
+ *
+ * @return {@code true} if the refactoring processor also updates references
+ */
+ public boolean isUpdateReferences() {
+ return mUpdateReferences;
+ }
+
+ /**
+ * Specifies if the refactoring processor also updates references. The
+ * default behavior is to update references.
+ *
+ * @param updateReferences {@code true} if the refactoring processor should
+ * also updates references
+ */
+ public void setUpdateReferences(boolean updateReferences) {
+ mUpdateReferences = updateReferences;
+ }
+
+ /**
+ * Checks the given new potential name and returns a {@link RefactoringStatus} indicating
+ * whether the potential new name is valid
+ *
+ * @param name the name to check
+ * @return a {@link RefactoringStatus} with the validation result
+ */
+ public RefactoringStatus checkNewName(String name) {
+ String error = mValidator.isValid(name);
+ if (error != null) {
+ return RefactoringStatus.createFatalErrorStatus(error);
+ }
+
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor pm,
+ CheckConditionsContext context) throws CoreException {
+ pm.beginTask("", 1);
+ try {
+ mRenameArguments = new RenameArguments(getNewName(), isUpdateReferences());
+ return new RefactoringStatus();
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException {
+ pm.beginTask("", 1);
+ try {
+ // Added by {@link RenameResourceParticipant}
+ return null;
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ public Object[] getElements() {
+ return new Object[0];
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "com.android.ide.renameResourceProcessor"; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getProcessorName() {
+ return "Rename Android Resource";
+ }
+
+ @Override
+ public boolean isApplicable() {
+ return true;
+ }
+
+ @Override
+ public RefactoringParticipant[] loadParticipants(RefactoringStatus status,
+ SharableParticipants shared) throws CoreException {
+ String[] affectedNatures = new String[] { AdtConstants.NATURE_DEFAULT };
+ String url = PREFIX_RESOURCE_REF + mType.getName() + '/' + mCurrentName;
+ return ParticipantManager.loadRenameParticipants(status, this, url, mRenameArguments,
+ null, affectedNatures, shared);
+ }
+} \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java
new file mode 100644
index 000000000..6ffe25d22
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.internal.ui.IJavaHelpContextIds;
+import org.eclipse.jdt.internal.ui.JavaPluginImages;
+import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameRefactoringWizard;
+import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Rename refactoring wizard for Android resources such as {@code @id/foo}
+ */
+@SuppressWarnings("restriction") // JDT refactoring UI
+public class RenameResourceWizard extends RenameRefactoringWizard {
+ private ResourceType mType;
+ private boolean mCanClear;
+
+ /**
+ * Constructs a new {@linkplain RenameResourceWizard}
+ *
+ * @param refactoring the refactoring
+ * @param type the type of resource being renamed
+ * @param canClear whether the user can clear the value
+ */
+ public RenameResourceWizard(
+ @NonNull RenameRefactoring refactoring,
+ @NonNull ResourceType type,
+ boolean canClear) {
+ super(refactoring,
+ "Rename Resource",
+ "Enter the new name for this resource",
+ JavaPluginImages.DESC_WIZBAN_REFACTOR_FIELD,
+ IJavaHelpContextIds.RENAME_FIELD_WIZARD_PAGE);
+ mType = type;
+ mCanClear = canClear;
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
+ RenameResourceProcessor processor = (RenameResourceProcessor) refactoring.getProcessor();
+ String name = processor.getNewName();
+ addPage(new RenameResourcePage(mType, name, mCanClear));
+ }
+
+ /**
+ * Initiates a renaming of a resource item
+ *
+ * @param shell the shell to parent the dialog to
+ * @param project the project containing the resource references
+ * @param type the type of resource
+ * @param currentName the name of the resource
+ * @param newName the new name, or null if not known
+ * @param canClear whether the name is allowed to be cleared
+ * @return false if initiating the rename failed
+ */
+ public static RenameResult renameResource(
+ @NonNull Shell shell,
+ @NonNull IProject project,
+ @NonNull ResourceType type,
+ @NonNull String currentName,
+ @Nullable String newName,
+ boolean canClear) {
+ try {
+ RenameResourceProcessor processor = new RenameResourceProcessor(project, type,
+ currentName, newName);
+ RenameRefactoring refactoring = new RenameRefactoring(processor);
+ if (!refactoring.isApplicable()) {
+ return RenameResult.unavailable();
+ }
+
+ if (!show(refactoring, processor, shell, type, canClear)) {
+ return RenameResult.canceled();
+ }
+ return RenameResult.name(processor.getNewName());
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return RenameResult.unavailable();
+ }
+
+ /**
+ * Show a refactoring dialog for the given resource refactoring operation
+ *
+ * @param refactoring the rename refactoring
+ * @param processor the field processor
+ * @param parent the parent shell
+ * @param type the resource type
+ * @param canClear whether the user is allowed to clear/reset the name to
+ * nothing
+ * @return true if the refactoring was performed, and false if it was
+ * canceled
+ * @throws CoreException if an unexpected error occurs
+ */
+ private static boolean show(
+ @NonNull RenameRefactoring refactoring,
+ @NonNull RenameResourceProcessor processor,
+ @NonNull Shell parent,
+ @NonNull ResourceType type,
+ boolean canClear) throws CoreException {
+ RefactoringSaveHelper saveHelper = new RefactoringSaveHelper(
+ RefactoringSaveHelper.SAVE_REFACTORING);
+ if (!saveHelper.saveEditors(parent)) {
+ return false;
+ }
+
+ try {
+ RenameResourceWizard wizard = new RenameResourceWizard(refactoring, type, canClear);
+ RefactoringWizardOpenOperation operation = new RefactoringWizardOpenOperation(wizard);
+ String dialogTitle = wizard.getDefaultPageTitle();
+ int result = operation.run(parent, dialogTitle == null ? "" : dialogTitle);
+ RefactoringStatus status = operation.getInitialConditionCheckingStatus();
+ if (status.hasFatalError()) {
+ return false;
+ }
+ if (result == RefactoringWizardOpenOperation.INITIAL_CONDITION_CHECKING_FAILED
+ || result == IDialogConstants.CANCEL_ID) {
+ saveHelper.triggerIncrementalBuild();
+ return false;
+ }
+
+ // Save modified resources; need to trigger R file regeneration
+ saveHelper.saveEditors(parent);
+
+ return true;
+ } catch (InterruptedException e) {
+ return false; // Canceled
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java
new file mode 100644
index 000000000..8ecb08836
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
+import static com.android.SdkConstants.ANDROID_PREFIX;
+import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
+import static com.android.SdkConstants.ATTR_NAME;
+import static com.android.SdkConstants.ATTR_TYPE;
+import static com.android.SdkConstants.TAG_ITEM;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceUrl;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
+import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameTypeWizard;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorExtension;
+import org.eclipse.ui.texteditor.ITextEditorExtension2;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Text action for XML files to invoke renaming
+ * <p>
+ * TODO: Handle other types of renaming: invoking class renaming when editing
+ * class names in layout files and manifest files, renaming attribute names when
+ * editing a styleable attribute, etc.
+ */
+@SuppressWarnings("restriction") // Java rename refactoring
+public final class RenameResourceXmlTextAction extends Action {
+ private final ITextEditor mEditor;
+
+ /**
+ * Creates a new {@linkplain RenameResourceXmlTextAction}
+ *
+ * @param editor the associated editor
+ */
+ public RenameResourceXmlTextAction(@NonNull ITextEditor editor) {
+ super("Rename");
+ mEditor = editor;
+ }
+
+ @Override
+ public void run() {
+ if (!validateEditorInputState()) {
+ return;
+ }
+ IFile file = getFile();
+ if (file == null) {
+ return;
+ }
+ IProject project = file.getProject();
+ if (project == null) {
+ return;
+ }
+ IDocument document = getDocument();
+ if (document == null) {
+ return;
+ }
+ ITextSelection selection = getSelection();
+ if (selection == null) {
+ return;
+ }
+
+ ResourceUrl resource = findResource(document, selection.getOffset());
+
+ if (resource == null) {
+ resource = findItemDefinition(document, selection.getOffset());
+ }
+
+ if (resource != null) {
+ ResourceType type = resource.type;
+ String name = resource.name;
+ Shell shell = mEditor.getSite().getShell();
+ boolean canClear = false;
+
+ RenameResourceWizard.renameResource(shell, project, type, name, null, canClear);
+ return;
+ }
+
+ String className = findClassName(document, file, selection.getOffset());
+ if (className != null) {
+ assert className.equals(className.trim());
+ IType type = findType(className, project);
+ if (type != null) {
+ RenameTypeProcessor processor = new RenameTypeProcessor(type);
+ //processor.setNewElementName(className);
+ processor.setUpdateQualifiedNames(true);
+ processor.setUpdateSimilarDeclarations(false);
+ //processor.setMatchStrategy(?);
+ //processor.setFilePatterns(patterns);
+ processor.setUpdateReferences(true);
+
+ RenameRefactoring refactoring = new RenameRefactoring(processor);
+ RenameTypeWizard wizard = new RenameTypeWizard(refactoring);
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ op.run(window.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ }
+ }
+
+ return;
+ }
+
+ // Fallback: tell user the cursor isn't in the right place
+ MessageDialog.openInformation(mEditor.getSite().getShell(),
+ "Rename",
+ "Operation unavailable on the current selection.\n"
+ + "Select an Android resource name or class.");
+ }
+
+ private boolean validateEditorInputState() {
+ if (mEditor instanceof ITextEditorExtension2)
+ return ((ITextEditorExtension2) mEditor).validateEditorInputState();
+ else if (mEditor instanceof ITextEditorExtension)
+ return !((ITextEditorExtension) mEditor).isEditorInputReadOnly();
+ else if (mEditor != null)
+ return mEditor.isEditable();
+ else
+ return false;
+ }
+
+ /**
+ * Searches for a resource URL around the caret, such as {@code @string/foo}
+ *
+ * @param document the document to search in
+ * @param offset the offset to search at
+ * @return a resource pair, or null if not found
+ */
+ @Nullable
+ public static ResourceUrl findResource(@NonNull IDocument document, int offset) {
+ try {
+ int max = document.getLength();
+ if (offset >= max) {
+ offset = max - 1;
+ } else if (offset < 0) {
+ offset = 0;
+ } else if (offset > 0) {
+ // If the caret is right after a resource name (meaning getChar(offset) points
+ // to the following character), back up
+ char c = document.getChar(offset);
+ if (!isValidResourceNameChar(c)) {
+ offset--;
+ }
+ }
+
+ int start = offset;
+ boolean valid = true;
+ for (; start >= 0; start--) {
+ char c = document.getChar(start);
+ if (c == '@' || c == '?') {
+ break;
+ } else if (!isValidResourceNameChar(c)) {
+ valid = false;
+ break;
+ }
+ }
+ if (valid) {
+ // Search forwards for the end
+ int end = start + 1;
+ for (; end < max; end++) {
+ char c = document.getChar(end);
+ if (!isValidResourceNameChar(c)) {
+ break;
+ }
+ }
+ if (end > start + 1) {
+ String url = document.get(start, end - start);
+
+ // Don't allow renaming framework resources -- @android:string/ok etc
+ if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) {
+ return null;
+ }
+
+ return ResourceUrl.parse(url);
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ private static boolean isValidResourceNameChar(char c) {
+ return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c);
+ }
+
+ /**
+ * Searches for an item definition around the caret, such as
+ * {@code <string name="foo">My String</string>}
+ */
+ private ResourceUrl findItemDefinition(IDocument document, int offset) {
+ Node node = DomUtilities.getNode(document, offset);
+ if (node == null) {
+ return null;
+ }
+ if (node.getNodeType() == Node.TEXT_NODE) {
+ node = node.getParentNode();
+ }
+ if (node == null || node.getNodeType() != Node.ELEMENT_NODE) {
+ return null;
+ }
+
+ Element element = (Element) node;
+ String name = element.getAttribute(ATTR_NAME);
+ if (name == null || name.isEmpty()) {
+ return null;
+ }
+ String typeString = element.getTagName();
+ if (TAG_ITEM.equals(typeString)) {
+ typeString = element.getAttribute(ATTR_TYPE);
+ if (typeString == null || typeString.isEmpty()) {
+ return null;
+ }
+ }
+ ResourceType type = ResourceType.getEnum(typeString);
+ if (type != null) {
+ return ResourceUrl.create(type, name, false, false);
+ }
+
+ return null;
+ }
+
+ /**
+ * Searches for a fully qualified class name around the caret, such as {@code foo.bar.MyClass}
+ *
+ * @param document the document to search in
+ * @param file the file, if known
+ * @param offset the offset to search at
+ * @return a resource pair, or null if not found
+ */
+ @Nullable
+ public static String findClassName(
+ @NonNull IDocument document,
+ @Nullable IFile file,
+ int offset) {
+ try {
+ int max = document.getLength();
+ if (offset >= max) {
+ offset = max - 1;
+ } else if (offset < 0) {
+ offset = 0;
+ } else if (offset > 0) {
+ // If the caret is right after a resource name (meaning getChar(offset) points
+ // to the following character), back up
+ char c = document.getChar(offset);
+ if (Character.isJavaIdentifierPart(c)) {
+ offset--;
+ }
+ }
+
+ int start = offset;
+ for (; start >= 0; start--) {
+ char c = document.getChar(start);
+ if (c == '"' || c == '<' || c == '/') {
+ start++;
+ break;
+ } else if (c != '.' && !Character.isJavaIdentifierPart(c)) {
+ return null;
+ }
+ }
+ // Search forwards for the end
+ int end = start + 1;
+ for (; end < max; end++) {
+ char c = document.getChar(end);
+ if (c != '.' && !Character.isJavaIdentifierPart(c)) {
+ if (c != '"' && c != '>' && !Character.isWhitespace(c)) {
+ return null;
+ }
+ break;
+ }
+ }
+ if (end > start + 1) {
+ String fqcn = document.get(start, end - start);
+ int dot = fqcn.indexOf('.');
+ if (dot == -1) { // Only support fully qualified names
+ return null;
+ }
+ if (dot == 0) { // Special case for manifests: prepend package
+ if (file != null && file.getName().equals(ANDROID_MANIFEST_XML)) {
+ ManifestInfo info = ManifestInfo.get(file.getProject());
+ return info.getPackage() + fqcn;
+ }
+ return null;
+ }
+
+ return fqcn;
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private IType findType(@NonNull String className, @NonNull IProject project) {
+ IType type = null;
+ try {
+ IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
+ type = javaProject.findType(className);
+ if (type == null || !type.exists()) {
+ return null;
+ }
+ if (!type.isBinary()) {
+ return type;
+ }
+ // See if this class is coming through a library project jar file and
+ // if so locate the real class
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null) {
+ List<IProject> libraries = projectState.getFullLibraryProjects();
+ for (IProject library : libraries) {
+ javaProject = BaseProjectHelper.getJavaProject(library);
+ type = javaProject.findType(className);
+ if (type != null && type.exists() && !type.isBinary()) {
+ return type;
+ }
+ }
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return null;
+ }
+
+ private ITextSelection getSelection() {
+ ISelectionProvider selectionProvider = mEditor.getSelectionProvider();
+ if (selectionProvider == null) {
+ return null;
+ }
+ ISelection selection = selectionProvider.getSelection();
+ if (!(selection instanceof ITextSelection)) {
+ return null;
+ }
+ return (ITextSelection) selection;
+ }
+
+ private IDocument getDocument() {
+ IDocumentProvider documentProvider = mEditor.getDocumentProvider();
+ if (documentProvider == null) {
+ return null;
+ }
+ IDocument document = documentProvider.getDocument(mEditor.getEditorInput());
+ if (document == null) {
+ return null;
+ }
+ return document;
+ }
+
+ @Nullable
+ private IFile getFile() {
+ IEditorInput input = mEditor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput) input;
+ return fileInput.getFile();
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java
new file mode 100644
index 000000000..ade346fa9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.refactorings.core;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+/**
+ * A result from a renaming operation
+ */
+public class RenameResult {
+ private boolean mCanceled;
+ private boolean mUnavailable;
+ private @Nullable String mName;
+ private boolean mClear;
+
+ /**
+ * Constructs a new rename result
+ */
+ private RenameResult() {
+ }
+
+ /**
+ * Creates a new blank {@linkplain RenameResult}
+ * @return a new result
+ */
+ @NonNull
+ public static RenameResult create() {
+ return new RenameResult();
+ }
+
+ /**
+ * Creates a new {@linkplain RenameResult} for a user canceled renaming operation
+ * @return a canceled operation
+ */
+ @NonNull
+ public static RenameResult canceled() {
+ return new RenameResult().setCanceled(true);
+ }
+
+ /**
+ * Creates a {@linkplain RenameResult} for a renaming operation that was
+ * not available (for example because the field attempted to be renamed
+ * does not yet exist (or does not exist any more)
+ *
+ * @return a new result
+ */
+ @NonNull
+ public static RenameResult unavailable() {
+ return new RenameResult().setUnavailable(true);
+ }
+
+ /**
+ * Creates a new {@linkplain RenameResult} for a successful renaming
+ * operation to the given name
+ *
+ * @param name the new name
+ * @return a new result
+ */
+ @NonNull
+ public static RenameResult name(@Nullable String name) {
+ return new RenameResult().setName(name);
+ }
+
+ /**
+ * Marks this result as canceled
+ *
+ * @param canceled whether the result was canceled
+ * @return this, for constructor chaining
+ */
+ @NonNull
+ public RenameResult setCanceled(boolean canceled) {
+ mCanceled = canceled;
+ return this;
+ }
+
+ /**
+ * Marks this result as unavailable
+ *
+ * @param unavailable whether this result was unavailable
+ * @return this, for constructor chaining
+ */
+ @NonNull
+ public RenameResult setUnavailable(boolean unavailable) {
+ mUnavailable = unavailable;
+ return this;
+ }
+
+ /**
+ * Sets the new name of the renaming operation
+ *
+ * @param name the new name
+ * @return this, for constructor chaining
+ */
+ @NonNull
+ public RenameResult setName(@Nullable String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Marks this result as clearing the name (reverting it back to the default)
+ *
+ * @param clear whether the name was cleared
+ * @return this, for constructor chaining
+ */
+ @NonNull
+ public RenameResult setCleared(boolean clear) {
+ mClear = clear;
+ return this;
+ }
+
+ /**
+ * Returns whether this result represents a canceled renaming operation
+ *
+ * @return true if the operation was canceled
+ */
+ public boolean isCanceled() {
+ return mCanceled;
+ }
+
+ /**
+ * Returns whether this result represents an unavailable renaming operation
+ *
+ * @return true if the operation was not available
+ */
+ public boolean isUnavailable() {
+ return mUnavailable;
+ }
+
+ /**
+ * Returns whether this result represents a renaming back to the default (possibly
+ * clear) name. In this case, {@link #getName()} will return {@code null}.
+ *
+ * @return true if the name should be reset
+ */
+ public boolean isCleared() {
+ return mClear;
+ }
+
+ /**
+ * Returns the new name.
+ *
+ * @return the new name
+ */
+ @Nullable
+ public String getName() {
+ return mName;
+ }
+} \ No newline at end of file