diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core')
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 |