diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings')
24 files changed, 8264 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 diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/EnabledTextEditGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/EnabledTextEditGroup.java new file mode 100644 index 000000000..15f6c4719 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/EnabledTextEditGroup.java @@ -0,0 +1,40 @@ +/* + * 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.extractstring; + +import org.eclipse.text.edits.TextEditGroup; + +/** + * A {@link TextEditGroup} that we want to enable or disable by default. + * This is used to propose a change that may not compile, so we'll create + * a change that is disabled. + * <p/> + * Disabling the change is done by the refactoring class when processing + * the text edit groups generated by the Java AST visitor. + */ +class EnabledTextEditGroup extends TextEditGroup { + private final boolean mEnabled; + + public EnabledTextEditGroup(String name, boolean enabled) { + super(name); + mEnabled = enabled; + } + + public boolean isEnabled() { + return mEnabled; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java new file mode 100644 index 000000000..14556fd9f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringAction.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import com.android.ide.eclipse.adt.AdtConstants; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.FileEditorInput; + +/* + * Quick Reference Link: + * http://www.eclipse.org/articles/article.php?file=Article-Unleashing-the-Power-of-Refactoring/index.html + * and + * http://www.ibm.com/developerworks/opensource/library/os-ecjdt/ + */ + +/** + * Action executed when the "Extract String" menu item is invoked. + * <p/> + * The intent of the action is to start a refactoring that extracts a source string and + * replaces it by an Android string resource ID. + * <p/> + * Workflow: + * <ul> + * <li> The action is currently located in the Refactoring menu in the main menu. + * <li> TODO: extend the popup refactoring menu in a Java or Android XML file. + * <li> The action is only enabled if the selection is 1 character or more. That is at least part + * of the string must be selected, it's not enough to just move the insertion point. This is + * a limitation due to {@link #selectionChanged(IAction, ISelection)} not being called when + * the insertion point is merely moved. TODO: address this limitation. + * <ul> The action gets the current {@link ISelection}. It also knows the current + * {@link IWorkbenchWindow}. However for the refactoring we are also interested in having the + * actual resource file. By looking at the Active Window > Active Page > Active Editor we + * can get the {@link IEditorInput} and find the {@link ICompilationUnit} (aka Java file) + * that is being edited. + * <ul> TODO: change this to find the {@link IFile} being manipulated. The {@link ICompilationUnit} + * can be inferred using {@link JavaCore#createCompilationUnitFrom(IFile)}. This will allow + * us to be able to work with a selection from an Android XML file later. + * <li> The action creates a new {@link ExtractStringRefactoring} and make it run on in a new + * {@link ExtractStringWizard}. + * <ul> + */ +public class ExtractStringAction implements IWorkbenchWindowActionDelegate { + + /** Keep track of the current workbench window. */ + private IWorkbenchWindow mWindow; + private ITextSelection mSelection; + private IEditorPart mEditor; + private IFile mFile; + + /** + * Keep track of the current workbench window. + */ + @Override + public void init(IWorkbenchWindow window) { + mWindow = window; + } + + @Override + public void dispose() { + // Nothing to do + } + + /** + * Examine the selection to determine if the action should be enabled or not. + * <p/> + * Keep a link to the relevant selection structure (i.e. a part of the Java AST). + */ + @Override + public void selectionChanged(IAction action, ISelection selection) { + + // Note, two kinds of selections are returned here: + // ITextSelection on a Java source window + // IStructuredSelection in the outline or navigator + // This simply deals with the refactoring based on a non-empty selection. + // At that point, just enable the action and later decide if it's valid when it actually + // runs since we don't have access to the AST yet. + + mSelection = null; + mFile = null; + + if (selection instanceof ITextSelection) { + mSelection = (ITextSelection) selection; + if (mSelection.getLength() > 0) { + mEditor = getActiveEditor(); + mFile = getSelectedFile(mEditor); + } + } + + action.setEnabled(mSelection != null && mFile != null); + } + + /** + * Create a new instance of our refactoring and a wizard to configure it. + */ + @Override + public void run(IAction action) { + if (mSelection != null && mFile != null) { + ExtractStringRefactoring ref = new ExtractStringRefactoring(mFile, mEditor, mSelection); + RefactoringWizard wizard = new ExtractStringWizard(ref, mFile.getProject()); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + op.run(mWindow.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + // Interrupted. Pass. + } + } + } + + /** + * Returns the active editor (hopefully matching our selection) or null. + */ + private IEditorPart getActiveEditor() { + IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (wwin != null) { + IWorkbenchPage page = wwin.getActivePage(); + if (page != null) { + return page.getActiveEditor(); + } + } + + return null; + } + + /** + * Returns the active {@link IFile} (hopefully matching our selection) or null. + * The file is only returned if it's a file from a project with an Android nature. + * <p/> + * At that point we do not try to analyze if the selection nor the file is suitable + * for the refactoring. This check is performed when the refactoring is invoked since + * it can then produce meaningful error messages as needed. + */ + private IFile getSelectedFile(IEditorPart editor) { + if (editor != null) { + IEditorInput input = editor.getEditorInput(); + + if (input instanceof FileEditorInput) { + FileEditorInput fi = (FileEditorInput) input; + IFile file = fi.getFile(); + if (file.exists()) { + IProject proj = file.getProject(); + try { + if (proj != null && proj.hasNature(AdtConstants.NATURE_DEFAULT)) { + return file; + } + } catch (CoreException e) { + // ignore + } + } + } + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java new file mode 100644 index 000000000..61bd06e81 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import org.eclipse.ltk.core.refactoring.RefactoringContribution; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; + +import java.util.Map; + +/** + * @see ExtractStringDescriptor + */ +public class ExtractStringContribution extends RefactoringContribution { + + /* (non-Javadoc) + * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int) + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public RefactoringDescriptor createDescriptor( + String id, + String project, + String description, + String comment, + Map arguments, + int flags) + throws IllegalArgumentException { + return new ExtractStringDescriptor(project, description, comment, arguments); + } + + @SuppressWarnings("rawtypes") + @Override + public Map retrieveArgumentMap(RefactoringDescriptor descriptor) { + if (descriptor instanceof ExtractStringDescriptor) { + return ((ExtractStringDescriptor) descriptor).getArguments(); + } + return super.retrieveArgumentMap(descriptor); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java new file mode 100644 index 000000000..190736aad --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringDescriptor.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringDescriptor; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; + +import java.util.Map; + +/** + * A descriptor that allows an {@link ExtractStringRefactoring} to be created from + * a previous instance of itself. + */ +public class ExtractStringDescriptor extends RefactoringDescriptor { + + public static final String ID = + "com.android.ide.eclipse.adt.refactoring.extract.string"; //$NON-NLS-1$ + + private final Map<String, String> mArguments; + + public ExtractStringDescriptor(String project, String description, String comment, + Map<String, String> arguments) { + super(ID, project, description, comment, + RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE //flags + ); + mArguments = arguments; + } + + public Map<String, String> getArguments() { + return mArguments; + } + + /** + * Creates a new refactoring instance for this refactoring descriptor based on + * an argument map. The argument map is created by the refactoring itself in + * {@link ExtractStringRefactoring#createChange(org.eclipse.core.runtime.IProgressMonitor)} + * <p/> + * This is apparently used to replay a refactoring. + * + * {@inheritDoc} + * + * @throws CoreException + */ + @Override + public Refactoring createRefactoring(RefactoringStatus status) throws CoreException { + try { + ExtractStringRefactoring ref = new ExtractStringRefactoring(mArguments); + return ref; + } catch (NullPointerException e) { + status.addFatalError("Failed to recreate ExtractStringRefactoring from descriptor"); + return null; + } + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java new file mode 100644 index 000000000..5ac5f5c4e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringInputPage.java @@ -0,0 +1,606 @@ +/* + * Copyright (C) 2009 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.extractstring; + + +import com.android.SdkConstants; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector; +import com.android.ide.eclipse.adt.internal.ui.ConfigurationSelector.SelectorMode; +import com.android.resources.ResourceFolderType; + +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.jface.wizard.WizardPage; +import org.eclipse.ltk.ui.refactoring.UserInputWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +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.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @see ExtractStringRefactoring + */ +class ExtractStringInputPage extends UserInputWizardPage { + + /** Last res file path used, shared across the session instances but specific to the + * current project. The default for unknown projects is {@link #DEFAULT_RES_FILE_PATH}. */ + private static HashMap<String, String> sLastResFilePath = new HashMap<String, String>(); + + /** The project where the user selection happened. */ + private final IProject mProject; + + /** Text field where the user enters the new ID to be generated or replaced with. */ + private Combo mStringIdCombo; + /** Text field where the user enters the new string value. */ + private Text mStringValueField; + /** The configuration selector, to select the resource path of the XML file. */ + private ConfigurationSelector mConfigSelector; + /** The combo to display the existing XML files or enter a new one. */ + private Combo mResFileCombo; + /** Checkbox asking whether to replace in all Java files. */ + private Button mReplaceAllJava; + /** Checkbox asking whether to replace in all XML files with same name but other res config */ + private Button mReplaceAllXml; + + /** Regex pattern to read a valid res XML file path. It checks that the are 2 folders and + * a leaf file name ending with .xml */ + private static final Pattern RES_XML_FILE_REGEX = Pattern.compile( + "/res/[a-z][a-zA-Z0-9_-]+/[^.]+\\.xml"); //$NON-NLS-1$ + /** Absolute destination folder root, e.g. "/res/" */ + private static final String RES_FOLDER_ABS = + AdtConstants.WS_RESOURCES + AdtConstants.WS_SEP; + /** Relative destination folder root, e.g. "res/" */ + private static final String RES_FOLDER_REL = + SdkConstants.FD_RESOURCES + AdtConstants.WS_SEP; + + private static final String DEFAULT_RES_FILE_PATH = "/res/values/strings.xml"; //$NON-NLS-1$ + + private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); + + private final OnConfigSelectorUpdated mOnConfigSelectorUpdated = new OnConfigSelectorUpdated(); + + private ModifyListener mValidateOnModify = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + }; + + private SelectionListener mValidateOnSelection = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + validatePage(); + } + }; + + public ExtractStringInputPage(IProject project) { + super("ExtractStringInputPage"); //$NON-NLS-1$ + mProject = project; + } + + /** + * Create the UI for the refactoring wizard. + * <p/> + * Note that at that point the initial conditions have been checked in + * {@link ExtractStringRefactoring}. + * <p/> + * + * Note: the special tag below defines this as the entry point for the WindowsDesigner Editor. + * @wbp.parser.entryPoint + */ + @Override + public void createControl(Composite parent) { + Composite content = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + content.setLayout(layout); + + createStringGroup(content); + createResFileGroup(content); + createOptionGroup(content); + + initUi(); + setControl(content); + } + + /** + * Creates the top group with the field to replace which string and by what + * and by which options. + * + * @param content A composite with a 1-column grid layout + */ + public void createStringGroup(Composite content) { + + final ExtractStringRefactoring ref = getOurRefactoring(); + + Group group = new Group(content, SWT.NONE); + group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + group.setText("New String"); + if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { + group.setText("String Replacement"); + } + + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + + // line: Textfield for string value (based on selection, if any) + + Label label = new Label(group, SWT.NONE); + label.setText("&String"); + + String selectedString = ref.getTokenString(); + + mStringValueField = new Text(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + mStringValueField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStringValueField.setText(selectedString != null ? selectedString : ""); //$NON-NLS-1$ + + ref.setNewStringValue(mStringValueField.getText()); + + mStringValueField.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + }); + + // line : Textfield for new ID + + label = new Label(group, SWT.NONE); + label.setText("ID &R.string."); + if (ref.getMode() == ExtractStringRefactoring.Mode.EDIT_SOURCE) { + label.setText("&Replace by R.string."); + } else if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { + label.setText("New &R.string."); + } + + mStringIdCombo = new Combo(group, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.DROP_DOWN); + mStringIdCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStringIdCombo.setText(guessId(selectedString)); + mStringIdCombo.forceFocus(); + + ref.setNewStringId(mStringIdCombo.getText().trim()); + + mStringIdCombo.addModifyListener(mValidateOnModify); + mStringIdCombo.addSelectionListener(mValidateOnSelection); + } + + /** + * Creates the lower group with the fields to choose the resource confirmation and + * the target XML file. + * + * @param content A composite with a 1-column grid layout + */ + private void createResFileGroup(Composite content) { + + Group group = new Group(content, SWT.NONE); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.grabExcessVerticalSpace = true; + group.setLayoutData(gd); + group.setText("XML resource to edit"); + + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + group.setLayout(layout); + + // line: selection of the res config + + Label label; + label = new Label(group, SWT.NONE); + label.setText("&Configuration:"); + + mConfigSelector = new ConfigurationSelector(group, SelectorMode.DEFAULT); + gd = new GridData(GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL); + gd.horizontalSpan = 2; + gd.widthHint = ConfigurationSelector.WIDTH_HINT; + gd.heightHint = ConfigurationSelector.HEIGHT_HINT; + mConfigSelector.setLayoutData(gd); + mConfigSelector.setOnChangeListener(mOnConfigSelectorUpdated); + + // line: selection of the output file + + label = new Label(group, SWT.NONE); + label.setText("Resource &file:"); + + mResFileCombo = new Combo(group, SWT.DROP_DOWN); + mResFileCombo.select(0); + mResFileCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mResFileCombo.addModifyListener(mOnConfigSelectorUpdated); + } + + /** + * Creates the bottom option groups with a few checkboxes. + * + * @param content A composite with a 1-column grid layout + */ + private void createOptionGroup(Composite content) { + Group options = new Group(content, SWT.NONE); + options.setText("Options"); + GridData gd_Options = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gd_Options.widthHint = 77; + options.setLayoutData(gd_Options); + options.setLayout(new GridLayout(1, false)); + + mReplaceAllJava = new Button(options, SWT.CHECK); + mReplaceAllJava.setToolTipText("When checked, the exact same string literal will be replaced in all Java files."); + mReplaceAllJava.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mReplaceAllJava.setText("Replace in all &Java files"); + mReplaceAllJava.addSelectionListener(mValidateOnSelection); + + mReplaceAllXml = new Button(options, SWT.CHECK); + mReplaceAllXml.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mReplaceAllXml.setToolTipText("When checked, string literals will be replaced in other XML resource files having the same name but located in different resource configuration folders."); + mReplaceAllXml.setText("Replace in all &XML files for different configuration"); + mReplaceAllXml.addSelectionListener(mValidateOnSelection); + } + + // -- Start of internal part ---------- + // Hide everything down-below from WindowsDesigner Editor + //$hide>>$ + + /** + * Init UI just after it has been created the first time. + */ + private void initUi() { + // set output file name to the last one used + String projPath = mProject.getFullPath().toPortableString(); + String filePath = sLastResFilePath.get(projPath); + + mResFileCombo.setText(filePath != null ? filePath : DEFAULT_RES_FILE_PATH); + mOnConfigSelectorUpdated.run(); + validatePage(); + } + + /** + * Utility method to guess a suitable new XML ID based on the selected string. + */ + public static String guessId(String text) { + if (text == null) { + return ""; //$NON-NLS-1$ + } + + // make lower case + text = text.toLowerCase(Locale.US); + + // everything not alphanumeric becomes an underscore + text = text.replaceAll("[^a-zA-Z0-9]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + + // the id must be a proper Java identifier, so it can't start with a number + if (text.length() > 0 && !Character.isJavaIdentifierStart(text.charAt(0))) { + text = "_" + text; //$NON-NLS-1$ + } + return text; + } + + /** + * Returns the {@link ExtractStringRefactoring} instance used by this wizard page. + */ + private ExtractStringRefactoring getOurRefactoring() { + return (ExtractStringRefactoring) getRefactoring(); + } + + /** + * Validates fields of the wizard input page. Displays errors as appropriate and + * enable the "Next" button (or not) by calling {@link #setPageComplete(boolean)}. + * + * If validation succeeds, this updates the text id & value in the refactoring object. + * + * @return True if the page has been positively validated. It may still have warnings. + */ + private boolean validatePage() { + boolean success = true; + + ExtractStringRefactoring ref = getOurRefactoring(); + + ref.setReplaceAllJava(mReplaceAllJava.getSelection()); + ref.setReplaceAllXml(mReplaceAllXml.isEnabled() && mReplaceAllXml.getSelection()); + + // Analyze fatal errors. + + String text = mStringIdCombo.getText().trim(); + if (text == null || text.length() < 1) { + setErrorMessage("Please provide a resource ID."); + success = false; + } else { + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + boolean ok = i == 0 ? + Character.isJavaIdentifierStart(c) : + Character.isJavaIdentifierPart(c); + if (!ok) { + setErrorMessage(String.format( + "The resource ID must be a valid Java identifier. The character %1$c at position %2$d is not acceptable.", + c, i+1)); + success = false; + break; + } + } + + // update the field in the refactoring object in case of success + if (success) { + ref.setNewStringId(text); + } + } + + String resFile = mResFileCombo.getText(); + if (success) { + if (resFile == null || resFile.length() == 0) { + setErrorMessage("A resource file name is required."); + success = false; + } else if (!RES_XML_FILE_REGEX.matcher(resFile).matches()) { + setErrorMessage("The XML file name is not valid."); + success = false; + } + } + + // Analyze info & warnings. + + if (success) { + setErrorMessage(null); + + ref.setTargetFile(resFile); + sLastResFilePath.put(mProject.getFullPath().toPortableString(), resFile); + + String idValue = mXmlHelper.valueOfStringId(mProject, resFile, text); + if (idValue != null) { + String msg = String.format("%1$s already contains a string ID '%2$s' with value '%3$s'.", + resFile, + text, + idValue); + if (ref.getMode() == ExtractStringRefactoring.Mode.SELECT_NEW_ID) { + setErrorMessage(msg); + success = false; + } else { + setMessage(msg, WizardPage.WARNING); + } + } else if (mProject.findMember(resFile) == null) { + setMessage( + String.format("File %2$s does not exist and will be created.", + text, resFile), + WizardPage.INFORMATION); + } else { + setMessage(null); + } + } + + if (success) { + // Also update the text value in case of success. + ref.setNewStringValue(mStringValueField.getText()); + } + + setPageComplete(success); + return success; + } + + private void updateStringValueCombo() { + String resFile = mResFileCombo.getText(); + Map<String, String> ids = mXmlHelper.getResIdsForFile(mProject, resFile); + + // get the current text from the combo, to make sure we don't change it + String currText = mStringIdCombo.getText(); + + // erase the choices and fill with the given ids + mStringIdCombo.removeAll(); + mStringIdCombo.setItems(ids.keySet().toArray(new String[ids.size()])); + + // set the current text to preserve it in case it changed + if (!currText.equals(mStringIdCombo.getText())) { + mStringIdCombo.setText(currText); + } + } + + private class OnConfigSelectorUpdated implements Runnable, ModifyListener { + + /** Regex pattern to parse a valid res path: it reads (/res/folder-name/)+(filename). */ + private final Pattern mPathRegex = Pattern.compile( + "(/res/[a-z][a-zA-Z0-9_-]+/)(.+)"); //$NON-NLS-1$ + + /** Temporary config object used to retrieve the Config Selector value. */ + private FolderConfiguration mTempConfig = new FolderConfiguration(); + + private HashMap<String, TreeSet<String>> mFolderCache = + new HashMap<String, TreeSet<String>>(); + private String mLastFolderUsedInCombo = null; + private boolean mInternalConfigChange; + private boolean mInternalFileComboChange; + + /** + * Callback invoked when the {@link ConfigurationSelector} has been changed. + * <p/> + * The callback does the following: + * <ul> + * <li> Examine the current file name to retrieve the XML filename, if any. + * <li> Recompute the path based on the configuration selector (e.g. /res/values-fr/). + * <li> Examine the path to retrieve all the files in it. Keep those in a local cache. + * <li> If the XML filename from step 1 is not in the file list, it's a custom file name. + * Insert it and sort it. + * <li> Re-populate the file combo with all the choices. + * <li> Select the original XML file. + */ + @Override + public void run() { + if (mInternalConfigChange) { + return; + } + + // get current leafname, if any + String leafName = ""; //$NON-NLS-1$ + String currPath = mResFileCombo.getText(); + Matcher m = mPathRegex.matcher(currPath); + if (m.matches()) { + // Note: groups 1 and 2 cannot be null. + leafName = m.group(2); + currPath = m.group(1); + } else { + // There was a path but it was invalid. Ignore it. + currPath = ""; //$NON-NLS-1$ + } + + // recreate the res path from the current configuration + mConfigSelector.getConfiguration(mTempConfig); + StringBuffer sb = new StringBuffer(RES_FOLDER_ABS); + sb.append(mTempConfig.getFolderName(ResourceFolderType.VALUES)); + sb.append(AdtConstants.WS_SEP); + + String newPath = sb.toString(); + + if (newPath.equals(currPath) && newPath.equals(mLastFolderUsedInCombo)) { + // Path has not changed. No need to reload. + return; + } + + // Get all the files at the new path + + TreeSet<String> filePaths = mFolderCache.get(newPath); + + if (filePaths == null) { + filePaths = new TreeSet<String>(); + + IFolder folder = mProject.getFolder(newPath); + if (folder != null && folder.exists()) { + try { + for (IResource res : folder.members()) { + String name = res.getName(); + if (res.getType() == IResource.FILE && name.endsWith(".xml")) { + filePaths.add(newPath + name); + } + } + } catch (CoreException e) { + // Ignore. + } + } + + mFolderCache.put(newPath, filePaths); + } + + currPath = newPath + leafName; + if (leafName.length() > 0 && !filePaths.contains(currPath)) { + filePaths.add(currPath); + } + + // Fill the combo + try { + mInternalFileComboChange = true; + + mResFileCombo.removeAll(); + + for (String filePath : filePaths) { + mResFileCombo.add(filePath); + } + + int index = -1; + if (leafName.length() > 0) { + index = mResFileCombo.indexOf(currPath); + if (index >= 0) { + mResFileCombo.select(index); + } + } + + if (index == -1) { + mResFileCombo.setText(currPath); + } + + mLastFolderUsedInCombo = newPath; + + } finally { + mInternalFileComboChange = false; + } + + // finally validate the whole page + updateStringValueCombo(); + validatePage(); + } + + /** + * Callback invoked when {@link ExtractStringInputPage#mResFileCombo} has been + * modified. + */ + @Override + public void modifyText(ModifyEvent e) { + if (mInternalFileComboChange) { + return; + } + + String wsFolderPath = mResFileCombo.getText(); + + // This is a custom path, we need to sanitize it. + // First it should start with "/res/". Then we need to make sure there are no + // relative paths, things like "../" or "./" or even "//". + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$ + wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + // We get "res/foo" from selections relative to the project when we want a "/res/foo" path. + if (wsFolderPath.startsWith(RES_FOLDER_REL)) { + wsFolderPath = RES_FOLDER_ABS + wsFolderPath.substring(RES_FOLDER_REL.length()); + + mInternalFileComboChange = true; + mResFileCombo.setText(wsFolderPath); + mInternalFileComboChange = false; + } + + if (wsFolderPath.startsWith(RES_FOLDER_ABS)) { + wsFolderPath = wsFolderPath.substring(RES_FOLDER_ABS.length()); + + int pos = wsFolderPath.indexOf(AdtConstants.WS_SEP_CHAR); + if (pos >= 0) { + wsFolderPath = wsFolderPath.substring(0, pos); + } + + String[] folderSegments = wsFolderPath.split(SdkConstants.RES_QUALIFIER_SEP); + + if (folderSegments.length > 0) { + String folderName = folderSegments[0]; + + if (folderName != null && !folderName.equals(wsFolderPath)) { + // update config selector + mInternalConfigChange = true; + mConfigSelector.setConfiguration(folderSegments); + mInternalConfigChange = false; + } + } + } + + updateStringValueCombo(); + validatePage(); + } + } + + // End of hiding from SWT Designer + //$hide<<$ + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringProposal.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringProposal.java new file mode 100644 index 000000000..5400be4e4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringProposal.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2011 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.extractstring; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.ui.text.java.IInvocationContext; +import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.text.TextSelection; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; + +/** + * Proposal for extracting strings in Java files + */ +public class ExtractStringProposal implements IJavaCompletionProposal { + private IInvocationContext mContext; + + public ExtractStringProposal(IInvocationContext context) { + mContext = context; + } + + @Override + public void apply(IDocument document) { + IEditorPart editor = AdtUtils.getActiveEditor(); + IFile file = AdtUtils.getActiveFile(); + if (editor == null || file == null) { + return; + } + + ASTNode coveringNode = mContext.getCoveringNode(); + int start = coveringNode.getStartPosition(); + int length = coveringNode.getLength(); + ITextSelection selection = new TextSelection(start, length); + + ExtractStringRefactoring refactoring = new ExtractStringRefactoring(file, editor, + selection); + + RefactoringWizard wizard = new ExtractStringWizard(refactoring, file.getProject()); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + op.run(window.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + } + } + + @Override + public Point getSelection(IDocument document) { + return null; + } + + @Override + public String getAdditionalProposalInfo() { + try { + ASTNode coveringNode = mContext.getCoveringNode(); + int start = coveringNode.getStartPosition(); + int length = coveringNode.getLength(); + IBuffer buffer = mContext.getCompilationUnit().getBuffer(); + StringBuilder sb = new StringBuilder(); + String string = buffer.getText(start, length); + string = ExtractStringRefactoring.unquoteAttrValue(string); + String token = ExtractStringInputPage.guessId(string); + + // Look up the beginning and the end of the line (outside of the extracted string) + // such that we can show a preview of the diff, e.g. if you have + // foo.setTitle("Hello"); we want to show foo.setTitle(R.string.hello); + // so we need to extract "foo.setTitle(" and ");". + + // Look backwards to the beginning of the line (and strip whitespace) + int i = start - 1; + while (i > 0) { + char c = buffer.getChar(i); + if (c == '\r' || (c == '\n')) { + break; + } + i--; + } + String linePrefix = buffer.getText(i + 1, start - (i + 1)).trim(); + + // Look forwards to the end of the line (and strip whitespace) + i = start + length; + while (i < buffer.getLength()) { + char c = buffer.getChar(i); + if (c == '\r' || (c == '\n')) { + break; + } + i++; + } + String lineSuffix = buffer.getText(start + length, i - (start + length)); + + // Should we show the replacement as just R.string.foo or + // context.getString(R.string.foo) ? + boolean useContext = false; + ASTNode parent = coveringNode.getParent(); + if (parent != null) { + int type = parent.getNodeType(); + if (type == ASTNode.ASSIGNMENT + || type == ASTNode.VARIABLE_DECLARATION_STATEMENT + || type == ASTNode.VARIABLE_DECLARATION_FRAGMENT + || type == ASTNode.VARIABLE_DECLARATION_EXPRESSION) { + useContext = true; + } + } + + // Display .java change: + sb.append("...<br>"); //$NON-NLS-1$ + sb.append(linePrefix); + sb.append("<b>"); //$NON-NLS-1$ + if (useContext) { + sb.append("context.getString("); //$NON-NLS-1$ + } + sb.append("R.string."); //$NON-NLS-1$ + sb.append(token); + if (useContext) { + sb.append(")"); //$NON-NLS-1$ + } + sb.append("</b>"); //$NON-NLS-1$ + sb.append(lineSuffix); + sb.append("<br>...<br>"); //$NON-NLS-1$ + + // Display strings.xml change: + sb.append("<br>"); //$NON-NLS-1$ + sb.append("<resources><br>"); //$NON-NLS-1$ + sb.append(" <b><string name=\""); //$NON-NLS-1$ + sb.append(token); + sb.append("\">"); //$NON-NLS-1$ + sb.append(string); + sb.append("</string></b><br>"); //$NON-NLS-1$ + sb.append("</resources>"); //$NON-NLS-1$ + + return sb.toString(); + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + } + + return "Initiates the Extract String refactoring operation"; + } + + @Override + public String getDisplayString() { + return "Extract String"; + } + + @Override + public Image getImage() { + return AdtPlugin.getAndroidLogo(); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public int getRelevance() { + return 80; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java new file mode 100644 index 000000000..db0b0967d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java @@ -0,0 +1,1933 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import static com.android.SdkConstants.QUOT_ENTITY; +import static com.android.SdkConstants.STRING_PREFIX; + +import com.android.SdkConstants; +import com.android.ide.common.res2.ValueXmlHelper; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ReferenceAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; + +import org.eclipse.core.resources.IContainer; +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.resources.ResourceAttributes; +import org.eclipse.core.resources.ResourcesPlugin; +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.core.runtime.Path; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.ToolFactory; +import org.eclipse.jdt.core.compiler.IScanner; +import org.eclipse.jdt.core.compiler.ITerminalSymbols; +import org.eclipse.jdt.core.compiler.InvalidInputException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.ChangeDescriptor; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.text.edits.InsertEdit; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.TextEditGroup; +import org.eclipse.ui.IEditorPart; +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.sse.core.internal.provisional.text.IStructuredDocumentRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; +import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; + +/** + * This refactoring extracts a string from a file and replaces it by an Android resource ID + * such as R.string.foo. + * <p/> + * There are a number of scenarios, which are not all supported yet. The workflow works as + * such: + * <ul> + * <li> User selects a string in a Java and invokes the {@link ExtractStringAction}. + * <li> The action finds the {@link ICompilationUnit} being edited as well as the current + * {@link ITextSelection}. The action creates a new instance of this refactoring as + * well as an {@link ExtractStringWizard} and runs the operation. + * <li> Step 1 of the refactoring is to check the preliminary conditions. Right now we check + * that the java source is not read-only and is in sync. We also try to find a string under + * the selection. If this fails, the refactoring is aborted. + * <li> On success, the wizard is shown, which lets the user input the new ID to use. + * <li> The wizard sets the user input values into this refactoring instance, e.g. the new string + * ID, the XML file to update, etc. The wizard does use the utility method + * {@link XmlStringFileHelper#valueOfStringId(IProject, String, String)} to check whether + * the new ID is already defined in the target XML file. + * <li> Once Preview or Finish is selected in the wizard, the + * {@link #checkFinalConditions(IProgressMonitor)} is called to double-check the user input + * and compute the actual changes. + * <li> When all changes are computed, {@link #createChange(IProgressMonitor)} is invoked. + * </ul> + * + * The list of changes are: + * <ul> + * <li> If the target XML does not exist, create it with the new string ID. + * <li> If the target XML exists, find the <resources> node and add the new string ID right after. + * If the node is <resources/>, it needs to be opened. + * <li> Create an AST rewriter to edit the source Java file and replace all occurrences by the + * new computed R.string.foo. Also need to rewrite imports to import R as needed. + * If there's already a conflicting R included, we need to insert the FQCN instead. + * <li> TODO: Have a pref in the wizard: [x] Change other XML Files + * <li> TODO: Have a pref in the wizard: [x] Change other Java Files + * </ul> + */ +@SuppressWarnings("restriction") +public class ExtractStringRefactoring extends Refactoring { + + public enum Mode { + /** + * the Extract String refactoring is called on an <em>existing</em> source file. + * Its purpose is then to get the selected string of the source and propose to + * change it by an XML id. The XML id may be a new one or an existing one. + */ + EDIT_SOURCE, + /** + * The Extract String refactoring is called without any source file. + * Its purpose is then to create a new XML string ID or select/modify an existing one. + */ + SELECT_ID, + /** + * The Extract String refactoring is called without any source file. + * Its purpose is then to create a new XML string ID. The ID must not already exist. + */ + SELECT_NEW_ID + } + + /** The {@link Mode} of operation of the refactoring. */ + private final Mode mMode; + /** Non-null when editing an Android Resource XML file: identifies the attribute name + * of the value being edited. When null, the source is an Android Java file. */ + private String mXmlAttributeName; + /** The file model being manipulated. + * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ + private final IFile mFile; + /** The editor. Non-null when invoked from {@link ExtractStringAction}. Null otherwise. */ + private final IEditorPart mEditor; + /** The project that contains {@link #mFile} and that contains the target XML file to modify. */ + private final IProject mProject; + /** The start of the selection in {@link #mFile}. + * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ + private final int mSelectionStart; + /** The end of the selection in {@link #mFile}. + * Value is -1 when not on {@link Mode#EDIT_SOURCE} mode. */ + private final int mSelectionEnd; + + /** The compilation unit, only defined if {@link #mFile} points to a usable Java source file. */ + private ICompilationUnit mUnit; + /** The actual string selected, after UTF characters have been escaped, good for display. + * Value is null when not on {@link Mode#EDIT_SOURCE} mode. */ + private String mTokenString; + + /** The XML string ID selected by the user in the wizard. */ + private String mXmlStringId; + /** The XML string value. Might be different than the initial selected string. */ + private String mXmlStringValue; + /** The path of the XML file that will define {@link #mXmlStringId}, selected by the user + * in the wizard. This is relative to the project, e.g. "/res/values/string.xml" */ + private String mTargetXmlFileWsPath; + /** True if we should find & replace in all Java files. */ + private boolean mReplaceAllJava; + /** True if we should find & replace in all XML files of the same name in other res configs + * (other than the main {@link #mTargetXmlFileWsPath}.) */ + private boolean mReplaceAllXml; + + /** The list of changes computed by {@link #checkFinalConditions(IProgressMonitor)} and + * used by {@link #createChange(IProgressMonitor)}. */ + private ArrayList<Change> mChanges; + + private XmlStringFileHelper mXmlHelper = new XmlStringFileHelper(); + + private static final String KEY_MODE = "mode"; //$NON-NLS-1$ + private static final String KEY_FILE = "file"; //$NON-NLS-1$ + private static final String KEY_PROJECT = "proj"; //$NON-NLS-1$ + private static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$ + private static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$ + private static final String KEY_TOK_ESC = "tok-esc"; //$NON-NLS-1$ + private static final String KEY_XML_ATTR_NAME = "xml-attr-name"; //$NON-NLS-1$ + private static final String KEY_RPLC_ALL_JAVA = "rplc-all-java"; //$NON-NLS-1$ + private static final String KEY_RPLC_ALL_XML = "rplc-all-xml"; //$NON-NLS-1$ + + /** + * This constructor is solely used by {@link ExtractStringDescriptor}, + * to replay a previous refactoring. + * <p/> + * To create a refactoring from code, please use one of the two other constructors. + * + * @param arguments A map previously created using {@link #createArgumentMap()}. + * @throws NullPointerException + */ + public ExtractStringRefactoring(Map<String, String> arguments) throws NullPointerException { + + mReplaceAllJava = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_JAVA)); + mReplaceAllXml = Boolean.parseBoolean(arguments.get(KEY_RPLC_ALL_XML)); + mMode = Mode.valueOf(arguments.get(KEY_MODE)); + + IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT)); + mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path); + + if (mMode == Mode.EDIT_SOURCE) { + path = Path.fromPortableString(arguments.get(KEY_FILE)); + mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path); + + mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START)); + mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END)); + mTokenString = arguments.get(KEY_TOK_ESC); + mXmlAttributeName = arguments.get(KEY_XML_ATTR_NAME); + } else { + mFile = null; + mSelectionStart = mSelectionEnd = -1; + mTokenString = null; + mXmlAttributeName = null; + } + + mEditor = null; + } + + private Map<String, String> createArgumentMap() { + HashMap<String, String> args = new HashMap<String, String>(); + args.put(KEY_RPLC_ALL_JAVA, Boolean.toString(mReplaceAllJava)); + args.put(KEY_RPLC_ALL_XML, Boolean.toString(mReplaceAllXml)); + args.put(KEY_MODE, mMode.name()); + args.put(KEY_PROJECT, mProject.getFullPath().toPortableString()); + if (mMode == Mode.EDIT_SOURCE) { + args.put(KEY_FILE, mFile.getFullPath().toPortableString()); + args.put(KEY_SEL_START, Integer.toString(mSelectionStart)); + args.put(KEY_SEL_END, Integer.toString(mSelectionEnd)); + args.put(KEY_TOK_ESC, mTokenString); + args.put(KEY_XML_ATTR_NAME, mXmlAttributeName); + } + return args; + } + + /** + * Constructor to use when the Extract String refactoring is called on an + * *existing* source file. Its purpose is then to get the selected string of + * the source and propose to change it by an XML id. The XML id may be a new one + * or an existing one. + * + * @param file The source file to process. Cannot be null. File must exist in workspace. + * @param editor The editor. + * @param selection The selection in the source file. Cannot be null or empty. + */ + public ExtractStringRefactoring(IFile file, IEditorPart editor, ITextSelection selection) { + mMode = Mode.EDIT_SOURCE; + mFile = file; + mEditor = editor; + mProject = file.getProject(); + mSelectionStart = selection.getOffset(); + mSelectionEnd = mSelectionStart + Math.max(0, selection.getLength() - 1); + } + + /** + * Constructor to use when the Extract String refactoring is called without + * any source file. Its purpose is then to create a new XML string ID. + * <p/> + * For example this is currently invoked by the ResourceChooser when + * the user wants to create a new string rather than select an existing one. + * + * @param project The project where the target XML file to modify is located. Cannot be null. + * @param enforceNew If true the XML ID must be a new one. + * If false, an existing ID can be used. + */ + public ExtractStringRefactoring(IProject project, boolean enforceNew) { + mMode = enforceNew ? Mode.SELECT_NEW_ID : Mode.SELECT_ID; + mFile = null; + mEditor = null; + mProject = project; + mSelectionStart = mSelectionEnd = -1; + } + + /** + * Sets the replacement string ID. Used by the wizard to set the user input. + */ + public void setNewStringId(String newStringId) { + mXmlStringId = newStringId; + } + + /** + * Sets the replacement string ID. Used by the wizard to set the user input. + */ + public void setNewStringValue(String newStringValue) { + mXmlStringValue = newStringValue; + } + + /** + * Sets the target file. This is a project path, e.g. "/res/values/strings.xml". + * Used by the wizard to set the user input. + */ + public void setTargetFile(String targetXmlFileWsPath) { + mTargetXmlFileWsPath = targetXmlFileWsPath; + } + + public void setReplaceAllJava(boolean replaceAllJava) { + mReplaceAllJava = replaceAllJava; + } + + public void setReplaceAllXml(boolean replaceAllXml) { + mReplaceAllXml = replaceAllXml; + } + + /** + * @see org.eclipse.ltk.core.refactoring.Refactoring#getName() + */ + @Override + public String getName() { + if (mMode == Mode.SELECT_ID) { + return "Create or Use Android String"; + } else if (mMode == Mode.SELECT_NEW_ID) { + return "Create New Android String"; + } + + return "Extract Android String"; + } + + public Mode getMode() { + return mMode; + } + + /** + * Gets the actual string selected, after UTF characters have been escaped, + * good for display. Value can be null. + */ + public String getTokenString() { + return mTokenString; + } + + /** Returns the XML string ID selected by the user in the wizard. */ + public String getXmlStringId() { + return mXmlStringId; + } + + /** + * Step 1 of 3 of the refactoring: + * Checks that the current selection meets the initial condition before the ExtractString + * wizard is shown. The check is supposed to be lightweight and quick. Note that at that + * point the wizard has not been created yet. + * <p/> + * Here we scan the source buffer to find the token matching the selection. + * The check is successful is a Java string literal is selected, the source is in sync + * and is not read-only. + * <p/> + * This is also used to extract the string to be modified, so that we can display it in + * the refactoring wizard. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkInitialConditions(org.eclipse.core.runtime.IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + + mUnit = null; + mTokenString = null; + + RefactoringStatus status = new RefactoringStatus(); + + try { + monitor.beginTask("Checking preconditions...", 6); + + if (mMode != Mode.EDIT_SOURCE) { + monitor.worked(6); + return status; + } + + if (!checkSourceFile(mFile, status, monitor)) { + return status; + } + + // Try to get a compilation unit from this file. If it fails, mUnit is null. + try { + mUnit = JavaCore.createCompilationUnitFrom(mFile); + + // Make sure the unit is not read-only, e.g. it's not a class file or inside a Jar + if (mUnit.isReadOnly()) { + status.addFatalError("The file is read-only, please make it writeable first."); + return status; + } + + // This is a Java file. Check if it contains the selection we want. + if (!findSelectionInJavaUnit(mUnit, status, monitor)) { + return status; + } + + } catch (Exception e) { + // That was not a Java file. Ignore. + } + + if (mUnit != null) { + monitor.worked(1); + return status; + } + + // Check this a Layout XML file and get the selection and its context. + if (mFile != null && SdkConstants.EXT_XML.equals(mFile.getFileExtension())) { + + // Currently we only support Android resource XML files, so they must have a path + // similar to + // project/res/<type>[-<configuration>]/*.xml + // project/AndroidManifest.xml + // There is no support for sub folders, so the segment count must be 4 or 2. + // We don't need to check the type folder name because a/ we only accept + // an AndroidXmlEditor source and b/ aapt generates a compilation error for + // unknown folders. + + IPath path = mFile.getFullPath(); + if ((path.segmentCount() == 4 && + path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) || + (path.segmentCount() == 2 && + path.segment(1).equalsIgnoreCase(SdkConstants.FN_ANDROID_MANIFEST_XML))) { + if (!findSelectionInXmlFile(mFile, status, monitor)) { + return status; + } + } + } + + if (!status.isOK()) { + status.addFatalError( + "Selection must be inside a Java source or an Android Layout XML file."); + } + + } finally { + monitor.done(); + } + + return status; + } + + /** + * Try to find the selected Java element in the compilation unit. + * + * If selection matches a string literal, capture it, otherwise add a fatal error + * to the status. + * + * On success, advance the monitor by 3. + * Returns status.isOK(). + */ + private boolean findSelectionInJavaUnit(ICompilationUnit unit, + RefactoringStatus status, IProgressMonitor monitor) { + try { + IBuffer buffer = unit.getBuffer(); + + IScanner scanner = ToolFactory.createScanner( + false, //tokenizeComments + false, //tokenizeWhiteSpace + false, //assertMode + false //recordLineSeparator + ); + scanner.setSource(buffer.getCharacters()); + monitor.worked(1); + + for(int token = scanner.getNextToken(); + token != ITerminalSymbols.TokenNameEOF; + token = scanner.getNextToken()) { + if (scanner.getCurrentTokenStartPosition() <= mSelectionStart && + scanner.getCurrentTokenEndPosition() >= mSelectionEnd) { + // found the token, but only keep if the right type + if (token == ITerminalSymbols.TokenNameStringLiteral) { + mTokenString = new String(scanner.getCurrentTokenSource()); + } + break; + } else if (scanner.getCurrentTokenStartPosition() > mSelectionEnd) { + // scanner is past the selection, abort. + break; + } + } + } catch (JavaModelException e1) { + // Error in unit.getBuffer. Ignore. + } catch (InvalidInputException e2) { + // Error in scanner.getNextToken. Ignore. + } finally { + monitor.worked(1); + } + + if (mTokenString != null) { + // As a literal string, the token should have surrounding quotes. Remove them. + // Note: unquoteAttrValue technically removes either " or ' paired quotes, whereas + // the Java token should only have " quotes. Since we know the type to be a string + // literal, there should be no confusion here. + mTokenString = unquoteAttrValue(mTokenString); + + // We need a non-empty string literal + if (mTokenString.length() == 0) { + mTokenString = null; + } + } + + if (mTokenString == null) { + status.addFatalError("Please select a Java string literal."); + } + + monitor.worked(1); + return status.isOK(); + } + + /** + * Try to find the selected XML element. This implementation replies on the refactoring + * originating from an Android Layout Editor. We rely on some internal properties of the + * Structured XML editor to retrieve file content to avoid parsing it again. We also rely + * on our specific Android XML model to get element & attribute descriptor properties. + * + * If selection matches a string literal, capture it, otherwise add a fatal error + * to the status. + * + * On success, advance the monitor by 1. + * Returns status.isOK(). + */ + private boolean findSelectionInXmlFile(IFile file, + RefactoringStatus status, + IProgressMonitor monitor) { + + try { + if (!(mEditor instanceof AndroidXmlEditor)) { + status.addFatalError("Only the Android XML Editor is currently supported."); + return status.isOK(); + } + + AndroidXmlEditor editor = (AndroidXmlEditor) mEditor; + IStructuredModel smodel = null; + Node node = null; + String currAttrName = null; + + try { + // See the portability note in AndroidXmlEditor#getModelForRead() javadoc. + smodel = editor.getModelForRead(); + if (smodel != null) { + // The structured model gives the us the actual XML Node element where the + // offset is. By using this Node, we can find the exact UiElementNode of our + // model and thus we'll be able to get the properties of the attribute -- to + // check if it accepts a string reference. This does not however tell us if + // the selection is actually in an attribute value, nor which attribute is + // being edited. + for(int offset = mSelectionStart; offset >= 0 && node == null; --offset) { + node = (Node) smodel.getIndexedRegion(offset); + } + + if (node == null) { + status.addFatalError( + "The selection does not match any element in the XML document."); + return status.isOK(); + } + + if (node.getNodeType() != Node.ELEMENT_NODE) { + status.addFatalError("The selection is not inside an actual XML element."); + return status.isOK(); + } + + IStructuredDocument sdoc = smodel.getStructuredDocument(); + if (sdoc != null) { + // Portability note: all the structured document implementation is + // under wst.sse.core.internal.provisional so we can expect it to change in + // a distant future if they start cleaning their codebase, however unlikely + // that is. + + int selStart = mSelectionStart; + IStructuredDocumentRegion region = + sdoc.getRegionAtCharacterOffset(selStart); + if (region != null && + DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + // Find if any sub-region representing an attribute contains the + // selection. If it does, returns the name of the attribute in + // currAttrName and returns the value in the field mTokenString. + currAttrName = findSelectionInRegion(region, selStart); + + if (mTokenString == null) { + status.addFatalError( + "The selection is not inside an actual XML attribute value."); + } + } + } + + if (mTokenString != null && node != null && currAttrName != null) { + + // Validate that the attribute accepts a string reference. + // This sets mTokenString to null by side-effect when it fails and + // adds a fatal error to the status as needed. + validateSelectedAttribute(editor, node, currAttrName, status); + + } else { + // We shouldn't get here: we're missing one of the token string, the node + // or the attribute name. All of them have been checked earlier so don't + // set any specific error. + mTokenString = null; + } + } + } catch (Throwable t) { + // Since we use some internal APIs, use a broad catch-all to report any + // unexpected issue rather than crash the whole refactoring. + status.addFatalError( + String.format("XML parsing error: %1$s", t.getMessage())); + } finally { + if (smodel != null) { + smodel.releaseFromRead(); + } + } + + } finally { + monitor.worked(1); + } + + return status.isOK(); + } + + /** + * The region gives us the textual representation of the XML element + * where the selection starts, split using sub-regions. We now just + * need to iterate through the sub-regions to find which one + * contains the actual selection. We're interested in an attribute + * value however when we find one we want to memorize the attribute + * name that was defined just before. + * + * @return When the cursor is on a valid attribute name or value, returns the string of + * attribute name. As a side-effect, returns the value of the attribute in {@link #mTokenString} + */ + private String findSelectionInRegion(IStructuredDocumentRegion region, int selStart) { + + String currAttrName = null; + + int startInRegion = selStart - region.getStartOffset(); + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String currAttrValue = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + currAttrName = region.getText(subRegion); + + // I like to select the attribute definition and invoke + // the extract string wizard. So if the selection is on + // the attribute name part, find the value that is just + // after and use it as if it were the selection. + + if (subRegion.getStart() <= startInRegion && + startInRegion < subRegion.getTextEnd()) { + // A well-formed attribute is composed of a name, + // an equal sign and the value. There can't be any space + // in between, which makes the parsing a lot easier. + if (i <= nb - 3 && + DOMRegionContext.XML_TAG_ATTRIBUTE_EQUALS.equals( + list.get(i + 1).getType())) { + subRegion = list.get(i + 2); + type = subRegion.getType(); + if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals( + type)) { + currAttrValue = region.getText(subRegion); + } + } + } + + } else if (subRegion.getStart() <= startInRegion && + startInRegion < subRegion.getTextEnd() && + DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + currAttrValue = region.getText(subRegion); + } + + if (currAttrValue != null) { + // We found the value. Only accept it if not empty + // and if we found an attribute name before. + String text = currAttrValue; + + // The attribute value contains XML quotes. Remove them. + text = unquoteAttrValue(text); + if (text.length() > 0 && currAttrName != null) { + // Setting mTokenString to non-null marks the fact we + // accept this attribute. + mTokenString = text; + } + + break; + } + } + + return currAttrName; + } + + /** + * Attribute values found as text for {@link DOMRegionContext#XML_TAG_ATTRIBUTE_VALUE} + * contain XML quotes. This removes the quotes (either single or double quotes). + * + * @param attrValue The attribute value, as extracted by + * {@link IStructuredDocumentRegion#getText(ITextRegion)}. + * Must not be null. + * @return The attribute value, without quotes. Whitespace is not trimmed, if any. + * String may be empty, but not null. + */ + static String unquoteAttrValue(String attrValue) { + int len = attrValue.length(); + int len1 = len - 1; + if (len >= 2 && + attrValue.charAt(0) == '"' && + attrValue.charAt(len1) == '"') { + attrValue = attrValue.substring(1, len1); + } else if (len >= 2 && + attrValue.charAt(0) == '\'' && + attrValue.charAt(len1) == '\'') { + attrValue = attrValue.substring(1, len1); + } + + return attrValue; + } + + /** + * Validates that the attribute accepts a string reference. + * This sets mTokenString to null by side-effect when it fails and + * adds a fatal error to the status as needed. + */ + private void validateSelectedAttribute(AndroidXmlEditor editor, Node node, + String attrName, RefactoringStatus status) { + UiElementNode rootUiNode = editor.getUiRootNode(); + UiElementNode currentUiNode = + rootUiNode == null ? null : rootUiNode.findXmlNode(node); + ReferenceAttributeDescriptor attrDesc = null; + + if (currentUiNode != null) { + // remove any namespace prefix from the attribute name + String name = attrName; + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + name = name.substring(pos + 1); + } + + for (UiAttributeNode attrNode : currentUiNode.getAllUiAttributes()) { + if (attrNode.getDescriptor().getXmlLocalName().equals(name)) { + AttributeDescriptor desc = attrNode.getDescriptor(); + if (desc instanceof ReferenceAttributeDescriptor) { + attrDesc = (ReferenceAttributeDescriptor) desc; + } + break; + } + } + } + + // The attribute descriptor is a resource reference. It must either accept + // of any resource type or specifically accept string types. + if (attrDesc != null && + (attrDesc.getResourceType() == null || + attrDesc.getResourceType() == ResourceType.STRING)) { + // We have one more check to do: is the current string value already + // an Android XML string reference? If so, we can't edit it. + if (mTokenString != null && mTokenString.startsWith("@")) { //$NON-NLS-1$ + int pos1 = 0; + if (mTokenString.length() > 1 && mTokenString.charAt(1) == '+') { + pos1++; + } + int pos2 = mTokenString.indexOf('/'); + if (pos2 > pos1) { + String kind = mTokenString.substring(pos1 + 1, pos2); + if (ResourceType.STRING.getName().equals(kind)) { + mTokenString = null; + status.addFatalError(String.format( + "The attribute %1$s already contains a %2$s reference.", + attrName, + kind)); + } + } + } + + if (mTokenString != null) { + // We're done with all our checks. mTokenString contains the + // current attribute value. We don't memorize the region nor the + // attribute, however we memorize the textual attribute name so + // that we can offer replacement for all its occurrences. + mXmlAttributeName = attrName; + } + + } else { + mTokenString = null; + status.addFatalError(String.format( + "The attribute %1$s does not accept a string reference.", + attrName)); + } + } + + /** + * Tests from org.eclipse.jdt.internal.corext.refactoringChecks#validateEdit() + * Might not be useful. + * + * On success, advance the monitor by 2. + * + * @return False if caller should abort, true if caller should continue. + */ + private boolean checkSourceFile(IFile file, + RefactoringStatus status, + IProgressMonitor monitor) { + // check whether the source file is in sync + if (!file.isSynchronized(IResource.DEPTH_ZERO)) { + status.addFatalError("The file is not synchronized. Please save it first."); + return false; + } + monitor.worked(1); + + // make sure we can write to it. + ResourceAttributes resAttr = file.getResourceAttributes(); + if (resAttr == null || resAttr.isReadOnly()) { + status.addFatalError("The file is read-only, please make it writeable first."); + return false; + } + monitor.worked(1); + + return true; + } + + /** + * Step 2 of 3 of the refactoring: + * Check the conditions once the user filled values in the refactoring wizard, + * then prepare the changes to be applied. + * <p/> + * In this case, most of the sanity checks are done by the wizard so essentially this + * should only be called if the wizard positively validated the user input. + * + * Here we do check that the target resource XML file either does not exists or + * is not read-only. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#checkFinalConditions(IProgressMonitor) + * + * @throws CoreException + */ + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + RefactoringStatus status = new RefactoringStatus(); + + try { + monitor.beginTask("Checking post-conditions...", 5); + + if (mXmlStringId == null || mXmlStringId.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing replacement string ID"); + } else if (mTargetXmlFileWsPath == null || mTargetXmlFileWsPath.length() <= 0) { + // this is not supposed to happen + status.addFatalError("Missing target xml file path"); + } + monitor.worked(1); + + // Either that resource must not exist or it must be a writable file. + IResource targetXml = getTargetXmlResource(mTargetXmlFileWsPath); + if (targetXml != null) { + if (targetXml.getType() != IResource.FILE) { + status.addFatalError( + String.format("XML file '%1$s' is not a file.", mTargetXmlFileWsPath)); + } else { + ResourceAttributes attr = targetXml.getResourceAttributes(); + if (attr != null && attr.isReadOnly()) { + status.addFatalError( + String.format("XML file '%1$s' is read-only.", + mTargetXmlFileWsPath)); + } + } + } + monitor.worked(1); + + if (status.hasError()) { + return status; + } + + mChanges = new ArrayList<Change>(); + + + // Prepare the change to create/edit the String ID in the res/values XML file. + if (!mXmlStringValue.equals( + mXmlHelper.valueOfStringId(mProject, mTargetXmlFileWsPath, mXmlStringId))) { + // We actually change it only if the ID doesn't exist yet or has a different value + Change change = createXmlChanges((IFile) targetXml, mXmlStringId, mXmlStringValue, + status, SubMonitor.convert(monitor, 1)); + if (change != null) { + mChanges.add(change); + } + } + + if (status.hasError()) { + return status; + } + + if (mMode == Mode.EDIT_SOURCE) { + List<Change> changes = null; + if (mXmlAttributeName != null) { + // Prepare the change to the Android resource XML file + changes = computeXmlSourceChanges(mFile, + mXmlStringId, + mTokenString, + mXmlAttributeName, + true, // allConfigurations + status, + monitor); + + } else if (mUnit != null) { + // Prepare the change to the Java compilation unit + changes = computeJavaChanges(mUnit, mXmlStringId, mTokenString, + status, SubMonitor.convert(monitor, 1)); + } + if (changes != null) { + mChanges.addAll(changes); + } + } + + if (mReplaceAllJava) { + String currentIdentifier = mUnit != null ? mUnit.getHandleIdentifier() : ""; //$NON-NLS-1$ + + SubMonitor submon = SubMonitor.convert(monitor, 1); + for (ICompilationUnit unit : findAllJavaUnits()) { + // Only process Java compilation units that exist, are not derived + // and are not read-only. + if (unit == null || !unit.exists()) { + continue; + } + IResource resource = unit.getResource(); + if (resource == null || resource.isDerived()) { + continue; + } + + // Ensure that we don't process the current compilation unit (processed + // as mUnit above) twice + if (currentIdentifier.equals(unit.getHandleIdentifier())) { + continue; + } + + ResourceAttributes attrs = resource.getResourceAttributes(); + if (attrs != null && attrs.isReadOnly()) { + continue; + } + + List<Change> changes = computeJavaChanges( + unit, mXmlStringId, mTokenString, + status, SubMonitor.convert(submon, 1)); + if (changes != null) { + mChanges.addAll(changes); + } + } + } + + if (mReplaceAllXml) { + SubMonitor submon = SubMonitor.convert(monitor, 1); + for (IFile xmlFile : findAllResXmlFiles()) { + if (xmlFile != null) { + List<Change> changes = computeXmlSourceChanges(xmlFile, + mXmlStringId, + mTokenString, + mXmlAttributeName, + false, // allConfigurations + status, + SubMonitor.convert(submon, 1)); + if (changes != null) { + mChanges.addAll(changes); + } + } + } + } + + monitor.worked(1); + } finally { + monitor.done(); + } + + return status; + } + + // --- XML changes --- + + /** + * Returns a foreach-compatible iterator over all XML files in the project's + * /res folder, excluding the target XML file (the one where we'll write/edit + * the string id). + */ + private Iterable<IFile> findAllResXmlFiles() { + return new Iterable<IFile>() { + @Override + public Iterator<IFile> iterator() { + return new Iterator<IFile>() { + final Queue<IFile> mFiles = new LinkedList<IFile>(); + final Queue<IResource> mFolders = new LinkedList<IResource>(); + IPath mFilterPath1 = null; + IPath mFilterPath2 = null; + { + // Filter out the XML file where we'll be writing the XML string id. + IResource filterRes = mProject.findMember(mTargetXmlFileWsPath); + if (filterRes != null) { + mFilterPath1 = filterRes.getFullPath(); + } + // Filter out the XML source file, if any (e.g. typically a layout) + if (mFile != null) { + mFilterPath2 = mFile.getFullPath(); + } + + // We want to process the manifest + IResource man = mProject.findMember("AndroidManifest.xml"); // TODO find a constant + if (man.exists() && man instanceof IFile && !man.equals(mFile)) { + mFiles.add((IFile) man); + } + + // Add all /res folders (technically we don't need to process /res/values + // XML files that contain resources/string elements, but it's easier to + // not filter them out.) + IFolder f = mProject.getFolder(AdtConstants.WS_RESOURCES); + if (f.exists()) { + try { + mFolders.addAll( + Arrays.asList(f.members(IContainer.EXCLUDE_DERIVED))); + } catch (CoreException e) { + // pass + } + } + } + + @Override + public boolean hasNext() { + if (!mFiles.isEmpty()) { + return true; + } + + while (!mFolders.isEmpty()) { + IResource res = mFolders.poll(); + if (res.exists() && res instanceof IFolder) { + IFolder f = (IFolder) res; + try { + getFileList(f); + if (!mFiles.isEmpty()) { + return true; + } + } catch (CoreException e) { + // pass + } + } + } + return false; + } + + private void getFileList(IFolder folder) throws CoreException { + for (IResource res : folder.members(IContainer.EXCLUDE_DERIVED)) { + // Only accept file resources which are not derived and actually exist + if (res.exists() && !res.isDerived() && res instanceof IFile) { + IFile file = (IFile) res; + // Must have an XML extension + if (SdkConstants.EXT_XML.equals(file.getFileExtension())) { + IPath p = file.getFullPath(); + // And not be either paths we want to filter out + if ((mFilterPath1 != null && mFilterPath1.equals(p)) || + (mFilterPath2 != null && mFilterPath2.equals(p))) { + continue; + } + mFiles.add(file); + } + } + } + } + + @Override + public IFile next() { + IFile file = mFiles.poll(); + hasNext(); + return file; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "This iterator does not support removal"); //$NON-NLS-1$ + } + }; + } + }; + } + + /** + * Internal helper that actually prepares the {@link Change} that adds the given + * ID to the given XML File. + * <p/> + * This does not actually modify the file. + * + * @param targetXml The file resource to modify. + * @param xmlStringId The new ID to insert. + * @param tokenString The old string, which will be the value in the XML string. + * @return A new {@link TextEdit} that describes how to change the file. + */ + private Change createXmlChanges(IFile targetXml, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor monitor) { + + TextFileChange xmlChange = new TextFileChange(getName(), targetXml); + xmlChange.setTextType(SdkConstants.EXT_XML); + + String error = ""; //$NON-NLS-1$ + TextEdit edit = null; + TextEditGroup editGroup = null; + + try { + if (!targetXml.exists()) { + // Kludge: use targetXml==null as a signal this is a new file being created + targetXml = null; + } + + edit = createXmlReplaceEdit(targetXml, xmlStringId, tokenString, status, + SubMonitor.convert(monitor, 1)); + } catch (IOException e) { + error = e.toString(); + } catch (CoreException e) { + // Failed to read file. Ignore. Will handle error below. + error = e.toString(); + } + + if (edit == null) { + status.addFatalError(String.format("Failed to modify file %1$s%2$s", + targetXml == null ? "" : targetXml.getFullPath(), //$NON-NLS-1$ + error == null ? "" : ": " + error)); //$NON-NLS-1$ + return null; + } + + editGroup = new TextEditGroup(targetXml == null ? "Create <string> in new XML file" + : "Insert <string> in XML file", + edit); + + xmlChange.setEdit(edit); + // The TextEditChangeGroup let the user toggle this change on and off later. + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, editGroup)); + + monitor.worked(1); + return xmlChange; + } + + /** + * Scan the XML file to find the best place where to insert the new string element. + * <p/> + * This handles a variety of cases, including replacing existing ids in place, + * adding the top resources element if missing and the XML PI if not present. + * It tries to preserve indentation when adding new elements at the end of an existing XML. + * + * @param file The XML file to modify, that must be present in the workspace. + * Pass null to create a change for a new file that doesn't exist yet. + * @param xmlStringId The new ID to insert. + * @param tokenString The old string, which will be the value in the XML string. + * @param status The in-out refactoring status. Used to log a more detailed error if the + * XML has a top element that is not a resources element. + * @param monitor A monitor to track progress. + * @return A new {@link TextEdit} for either a replace or an insert operation, or null in case + * of error. + * @throws CoreException - if the file's contents or description can not be read. + * @throws IOException - if the file's contents can not be read or its detected encoding does + * not support its contents. + */ + private TextEdit createXmlReplaceEdit(IFile file, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor monitor) + throws IOException, CoreException { + + IModelManager modelMan = StructuredModelManager.getModelManager(); + + final String NODE_RESOURCES = SdkConstants.TAG_RESOURCES; + final String NODE_STRING = SdkConstants.TAG_STRING; + final String ATTR_NAME = SdkConstants.ATTR_NAME; + + + // Scan the source to find the best insertion point. + + // 1- The most common case we need to handle is the one of inserting at the end + // of a valid XML document, respecting the whitespace last used. + // + // Ideally we have this structure: + // <xml ...> + // <resource> + // ...ws1...<string>blah</string>...ws2... + // </resource> + // + // where ws1 and ws2 are the whitespace respectively before and after the last element + // just before the closing </resource>. + // In this case we want to generate the new string just before ws2...</resource> with + // the same whitespace as ws1. + // + // 2- Another expected case is there's already an existing string which "name" attribute + // equals to xmlStringId and we just want to replace its value. + // + // Other cases we need to handle: + // 3- There is no element at all -> create a full new <resource>+<string> content. + // 4- There is <resource/>, that is the tag is not opened. This can be handled as the + // previous case, generating full content but also replacing <resource/>. + // 5- There is a top element that is not <resource>. That's a fatal error and we abort. + + IStructuredModel smodel = null; + + // Single and double quotes must be escaped in the <string>value</string> declaration + tokenString = ValueXmlHelper.escapeResourceString(tokenString); + + try { + IStructuredDocument sdoc = null; + boolean checkTopElement = true; + boolean replaceStringContent = false; + boolean hasPiXml = false; + int newResStart = 0; + int newResLength = 0; + String lineSep = "\n"; //$NON-NLS-1$ + + if (file != null) { + smodel = modelMan.getExistingModelForRead(file); + if (smodel != null) { + sdoc = smodel.getStructuredDocument(); + } else if (smodel == null) { + // The model is not currently open. + if (file.exists()) { + sdoc = modelMan.createStructuredDocumentFor(file); + } else { + sdoc = modelMan.createNewStructuredDocumentFor(file); + } + } + } + + if (sdoc == null && file != null) { + // Get a document matching the actual saved file + sdoc = modelMan.createStructuredDocumentFor(file); + } + + if (sdoc != null) { + String wsBefore = ""; //$NON-NLS-1$ + String lastWs = null; + + lineSep = sdoc.getLineDelimiter(); + if (lineSep == null || lineSep.length() == 0) { + // That wasn't too useful, let's go back to a reasonable default + lineSep = "\n"; //$NON-NLS-1$ + } + + for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) { + String type = regions.getType(); + + if (DOMRegionContext.XML_CONTENT.equals(type)) { + + if (replaceStringContent) { + // Generate a replacement for a <string> value matching the string ID. + return new ReplaceEdit( + regions.getStartOffset(), regions.getLength(), tokenString); + } + + // Otherwise capture what should be whitespace content + lastWs = regions.getFullText(); + continue; + + } else if (DOMRegionContext.XML_PI_OPEN.equals(type) && !hasPiXml) { + + int nb = regions.getNumberOfRegions(); + ITextRegionList list = regions.getRegions(); + for (int i = 0; i < nb; i++) { + ITextRegion region = list.get(i); + type = region.getType(); + if (DOMRegionContext.XML_TAG_NAME.equals(type)) { + String name = regions.getText(region); + if ("xml".equals(name)) { //$NON-NLS-1$ + hasPiXml = true; + break; + } + } + } + continue; + + } else if (!DOMRegionContext.XML_TAG_NAME.equals(type)) { + // ignore things which are not a tag nor text content (such as comments) + continue; + } + + int nb = regions.getNumberOfRegions(); + ITextRegionList list = regions.getRegions(); + + String name = null; + String attrName = null; + String attrValue = null; + boolean isEmptyTag = false; + boolean isCloseTag = false; + + for (int i = 0; i < nb; i++) { + ITextRegion region = list.get(i); + type = region.getType(); + + if (DOMRegionContext.XML_END_TAG_OPEN.equals(type)) { + isCloseTag = true; + } else if (DOMRegionContext.XML_EMPTY_TAG_CLOSE.equals(type)) { + isEmptyTag = true; + } else if (DOMRegionContext.XML_TAG_NAME.equals(type)) { + name = regions.getText(region); + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type) && + NODE_STRING.equals(name)) { + // Record the attribute names into a <string> element. + attrName = regions.getText(region); + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type) && + ATTR_NAME.equals(attrName)) { + // Record the value of a <string name=...> attribute + attrValue = regions.getText(region); + + if (attrValue != null && + unquoteAttrValue(attrValue).equals(xmlStringId)) { + // We found a <string name=> matching the string ID to replace. + // We'll generate a replacement when we process the string value + // (that is the next XML_CONTENT region.) + replaceStringContent = true; + } + } + } + + if (checkTopElement) { + // Check the top element has a resource name + checkTopElement = false; + if (!NODE_RESOURCES.equals(name)) { + status.addFatalError( + String.format("XML file lacks a <resource> tag: %1$s", + mTargetXmlFileWsPath)); + return null; + + } + + if (isEmptyTag) { + // The top element is an empty "<resource/>" tag. We need to do + // a full new resource+string replacement. + newResStart = regions.getStartOffset(); + newResLength = regions.getLength(); + } + } + + if (NODE_RESOURCES.equals(name)) { + if (isCloseTag) { + // We found the </resource> tag and we want + // to insert just before this one. + + StringBuilder content = new StringBuilder(); + content.append(wsBefore) + .append("<string name=\"") //$NON-NLS-1$ + .append(xmlStringId) + .append("\">") //$NON-NLS-1$ + .append(tokenString) + .append("</string>"); //$NON-NLS-1$ + + // Backup to insert before the whitespace preceding </resource> + IStructuredDocumentRegion insertBeforeReg = regions; + while (true) { + IStructuredDocumentRegion previous = insertBeforeReg.getPrevious(); + if (previous != null && + DOMRegionContext.XML_CONTENT.equals(previous.getType()) && + previous.getText().trim().length() == 0) { + insertBeforeReg = previous; + } else { + break; + } + } + if (insertBeforeReg == regions) { + // If we have not found any whitespace before </resources>, + // at least add a line separator. + content.append(lineSep); + } + + return new InsertEdit(insertBeforeReg.getStartOffset(), + content.toString()); + } + } else { + // For any other tag than <resource>, capture whitespace before and after. + if (!isCloseTag) { + wsBefore = lastWs; + } + } + } + } + + // We reach here either because there's no XML content at all or because + // there's an empty <resource/>. + // Provide a full new resource+string replacement. + StringBuilder content = new StringBuilder(); + if (!hasPiXml) { + content.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); //$NON-NLS-1$ + content.append(lineSep); + } else if (newResLength == 0 && sdoc != null) { + // If inserting at the end, check if the last region is some whitespace. + // If there's no newline, insert one ourselves. + IStructuredDocumentRegion lastReg = sdoc.getLastStructuredDocumentRegion(); + if (lastReg != null && lastReg.getText().indexOf('\n') == -1) { + content.append('\n'); + } + } + + // FIXME how to access formatting preferences to generate the proper indentation? + content.append("<resources>").append(lineSep); //$NON-NLS-1$ + content.append(" <string name=\"") //$NON-NLS-1$ + .append(xmlStringId) + .append("\">") //$NON-NLS-1$ + .append(tokenString) + .append("</string>") //$NON-NLS-1$ + .append(lineSep); + content.append("</resources>").append(lineSep); //$NON-NLS-1$ + + if (newResLength > 0) { + // Replace existing piece + return new ReplaceEdit(newResStart, newResLength, content.toString()); + } else { + // Insert at the end. + int offset = sdoc == null ? 0 : sdoc.getLength(); + return new InsertEdit(offset, content.toString()); + } + } catch (IOException e) { + // This is expected to happen and is properly reported to the UI. + throw e; + } catch (CoreException e) { + // This is expected to happen and is properly reported to the UI. + throw e; + } catch (Throwable t) { + // Since we use some internal APIs, use a broad catch-all to report any + // unexpected issue rather than crash the whole refactoring. + status.addFatalError( + String.format("XML replace error: %1$s", t.getMessage())); + } finally { + if (smodel != null) { + smodel.releaseFromRead(); + } + } + + return null; + } + + /** + * Computes the changes to be made to the source Android XML file and + * returns a list of {@link Change}. + * <p/> + * This function scans an XML file, looking for an attribute value equals to + * <code>tokenString</code>. If non null, <code>xmlAttrName</code> limit the search + * to only attributes that have that name. + * If found, a change is made to replace each occurrence of <code>tokenString</code> + * by a new "@string/..." using the new <code>xmlStringId</code>. + * + * @param sourceFile The file to process. + * A status error will be generated if it does not exists. + * Must not be null. + * @param tokenString The string to find. Must not be null or empty. + * @param xmlAttrName Optional attribute name to limit the search. Can be null. + * @param allConfigurations True if this function should can all XML files with the same + * name and the same resource type folder but with different configurations. + * @param status Status used to report fatal errors. + * @param monitor Used to log progress. + */ + private List<Change> computeXmlSourceChanges(IFile sourceFile, + String xmlStringId, + String tokenString, + String xmlAttrName, + boolean allConfigurations, + RefactoringStatus status, + IProgressMonitor monitor) { + + if (!sourceFile.exists()) { + status.addFatalError(String.format("XML file '%1$s' does not exist.", + sourceFile.getFullPath().toOSString())); + return null; + } + + // We shouldn't be trying to replace a null or empty string. + assert tokenString != null && tokenString.length() > 0; + if (tokenString == null || tokenString.length() == 0) { + return null; + } + + // Note: initially this method was only processing files using a pattern + // /project/res/<type>-<configuration>/<filename.xml> + // However the last version made that more generic to be able to process any XML + // files. We should probably revisit and simplify this later. + HashSet<IFile> files = new HashSet<IFile>(); + files.add(sourceFile); + + if (allConfigurations && SdkConstants.EXT_XML.equals(sourceFile.getFileExtension())) { + IPath path = sourceFile.getFullPath(); + if (path.segmentCount() == 4 && path.segment(1).equals(SdkConstants.FD_RESOURCES)) { + IProject project = sourceFile.getProject(); + String filename = path.segment(3); + String initialTypeName = path.segment(2); + ResourceFolderType type = ResourceFolderType.getFolderType(initialTypeName); + + IContainer res = sourceFile.getParent().getParent(); + if (type != null && res != null && res.getType() == IResource.FOLDER) { + try { + for (IResource r : res.members()) { + if (r != null && r.getType() == IResource.FOLDER) { + String name = r.getName(); + // Skip the initial folder name, it's already in the list. + if (!name.equals(initialTypeName)) { + // Only accept the same folder type (e.g. layout-*) + ResourceFolderType t = + ResourceFolderType.getFolderType(name); + if (type.equals(t)) { + // recompute the path + IPath p = res.getProjectRelativePath().append(name). + append(filename); + IResource f = project.findMember(p); + if (f != null && f instanceof IFile) { + files.add((IFile) f); + } + } + } + } + } + } catch (CoreException e) { + // Ignore. + } + } + } + } + + SubMonitor subMonitor = SubMonitor.convert(monitor, Math.min(1, files.size())); + + ArrayList<Change> changes = new ArrayList<Change>(); + + // Portability note: getModelManager is part of wst.sse.core however the + // interface returned is part of wst.sse.core.internal.provisional so we can + // expect it to change in a distant future if they start cleaning their codebase, + // however unlikely that is. + IModelManager modelManager = StructuredModelManager.getModelManager(); + + for (IFile file : files) { + + IStructuredModel smodel = null; + MultiTextEdit multiEdit = null; + TextFileChange xmlChange = null; + ArrayList<TextEditGroup> editGroups = null; + + try { + IStructuredDocument sdoc = null; + + smodel = modelManager.getExistingModelForRead(file); + if (smodel != null) { + sdoc = smodel.getStructuredDocument(); + } else if (smodel == null) { + // The model is not currently open. + if (file.exists()) { + sdoc = modelManager.createStructuredDocumentFor(file); + } else { + sdoc = modelManager.createNewStructuredDocumentFor(file); + } + } + + if (sdoc == null) { + status.addFatalError("XML structured document not found"); //$NON-NLS-1$ + continue; + } + + multiEdit = new MultiTextEdit(); + editGroups = new ArrayList<TextEditGroup>(); + xmlChange = new TextFileChange(getName(), file); + xmlChange.setTextType("xml"); //$NON-NLS-1$ + + String quotedReplacement = quotedAttrValue(STRING_PREFIX + xmlStringId); + + // Prepare the change set + for (IStructuredDocumentRegion regions : sdoc.getStructuredDocumentRegions()) { + // Only look at XML "top regions" + if (!DOMRegionContext.XML_TAG_NAME.equals(regions.getType())) { + continue; + } + + int nb = regions.getNumberOfRegions(); + ITextRegionList list = regions.getRegions(); + String lastAttrName = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + // Memorize the last attribute name seen + lastAttrName = regions.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + // Check this is the attribute and the original string + String text = regions.getText(subRegion); + + // Remove " or ' quoting present in the attribute value + text = unquoteAttrValue(text); + + if (tokenString.equals(text) && + (xmlAttrName == null || xmlAttrName.equals(lastAttrName))) { + + // Found an occurrence. Create a change for it. + TextEdit edit = new ReplaceEdit( + regions.getStartOffset() + subRegion.getStart(), + subRegion.getTextLength(), + quotedReplacement); + TextEditGroup editGroup = new TextEditGroup( + "Replace attribute string by ID", + edit); + + multiEdit.addChild(edit); + editGroups.add(editGroup); + } + } + } + } + } catch (Throwable t) { + // Since we use some internal APIs, use a broad catch-all to report any + // unexpected issue rather than crash the whole refactoring. + status.addFatalError( + String.format("XML refactoring error: %1$s", t.getMessage())); + } finally { + if (smodel != null) { + smodel.releaseFromRead(); + } + + if (multiEdit != null && + xmlChange != null && + editGroups != null && + multiEdit.hasChildren()) { + xmlChange.setEdit(multiEdit); + for (TextEditGroup group : editGroups) { + xmlChange.addTextEditChangeGroup( + new TextEditChangeGroup(xmlChange, group)); + } + changes.add(xmlChange); + } + subMonitor.worked(1); + } + } // for files + + if (changes.size() > 0) { + return changes; + } + return null; + } + + /** + * Returns a quoted attribute value suitable to be placed after an attributeName= + * statement in an XML stream. + * + * According to http://www.w3.org/TR/2008/REC-xml-20081126/#NT-AttValue + * the attribute value can be either quoted using ' or " and the corresponding + * entities ' or " must be used inside. + */ + private String quotedAttrValue(String attrValue) { + if (attrValue.indexOf('"') == -1) { + // no double-quotes inside, use double-quotes around. + return '"' + attrValue + '"'; + } + if (attrValue.indexOf('\'') == -1) { + // no single-quotes inside, use single-quotes around. + return '\'' + attrValue + '\''; + } + // If we get here, there's a mix. Opt for double-quote around and replace + // inner double-quotes. + attrValue = attrValue.replace("\"", QUOT_ENTITY); //$NON-NLS-1$ + return '"' + attrValue + '"'; + } + + // --- Java changes --- + + /** + * Returns a foreach compatible iterator over all ICompilationUnit in the project. + */ + private Iterable<ICompilationUnit> findAllJavaUnits() { + final IJavaProject javaProject = JavaCore.create(mProject); + + return new Iterable<ICompilationUnit>() { + @Override + public Iterator<ICompilationUnit> iterator() { + return new Iterator<ICompilationUnit>() { + final Queue<ICompilationUnit> mUnits = new LinkedList<ICompilationUnit>(); + final Queue<IPackageFragment> mFragments = new LinkedList<IPackageFragment>(); + { + try { + IPackageFragment[] tmpFrags = javaProject.getPackageFragments(); + if (tmpFrags != null && tmpFrags.length > 0) { + mFragments.addAll(Arrays.asList(tmpFrags)); + } + } catch (JavaModelException e) { + // pass + } + } + + @Override + public boolean hasNext() { + if (!mUnits.isEmpty()) { + return true; + } + + while (!mFragments.isEmpty()) { + try { + IPackageFragment fragment = mFragments.poll(); + if (fragment.getKind() == IPackageFragmentRoot.K_SOURCE) { + ICompilationUnit[] tmpUnits = fragment.getCompilationUnits(); + if (tmpUnits != null && tmpUnits.length > 0) { + mUnits.addAll(Arrays.asList(tmpUnits)); + return true; + } + } + } catch (JavaModelException e) { + // pass + } + } + return false; + } + + @Override + public ICompilationUnit next() { + ICompilationUnit unit = mUnits.poll(); + hasNext(); + return unit; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "This iterator does not support removal"); //$NON-NLS-1$ + } + }; + } + }; + } + + /** + * Computes the changes to be made to Java file(s) and returns a list of {@link Change}. + * <p/> + * This function scans a Java compilation unit using {@link ReplaceStringsVisitor}, looking + * for a string literal equals to <code>tokenString</code>. + * If found, a change is made to replace each occurrence of <code>tokenString</code> by + * a piece of Java code that somehow accesses R.string.<code>xmlStringId</code>. + * + * @param unit The compilated unit to process. Must not be null. + * @param tokenString The string to find. Must not be null or empty. + * @param status Status used to report fatal errors. + * @param monitor Used to log progress. + */ + private List<Change> computeJavaChanges(ICompilationUnit unit, + String xmlStringId, + String tokenString, + RefactoringStatus status, + SubMonitor monitor) { + + // We shouldn't be trying to replace a null or empty string. + assert tokenString != null && tokenString.length() > 0; + if (tokenString == null || tokenString.length() == 0) { + return null; + } + + // Get the Android package name from the Android Manifest. We need it to create + // the FQCN of the R class. + String packageName = null; + String error = null; + IResource manifestFile = mProject.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestFile == null || manifestFile.getType() != IResource.FILE) { + error = "File not found"; + } else { + ManifestData manifestData = AndroidManifestHelper.parseForData((IFile) manifestFile); + if (manifestData == null) { + error = "Invalid content"; + } else { + packageName = manifestData.getPackage(); + if (packageName == null) { + error = "Missing package definition"; + } + } + } + + if (error != null) { + status.addFatalError( + String.format("Failed to parse file %1$s: %2$s.", + manifestFile == null ? "" : manifestFile.getFullPath(), //$NON-NLS-1$ + error)); + return null; + } + + // Right now the changes array will contain one TextFileChange at most. + ArrayList<Change> changes = new ArrayList<Change>(); + + // This is the unit that will be modified. + TextFileChange change = new TextFileChange(getName(), (IFile) unit.getResource()); + change.setTextType("java"); //$NON-NLS-1$ + + // Create an AST for this compilation unit + ASTParser parser = ASTParser.newParser(AST.JLS3); + parser.setProject(unit.getJavaProject()); + parser.setSource(unit); + parser.setResolveBindings(true); + ASTNode node = parser.createAST(monitor.newChild(1)); + + // The ASTNode must be a CompilationUnit, by design + if (!(node instanceof CompilationUnit)) { + status.addFatalError(String.format("Internal error: ASTNode class %s", //$NON-NLS-1$ + node.getClass())); + return null; + } + + // ImportRewrite will allow us to add the new type to the imports and will resolve + // what the Java source must reference, e.g. the FQCN or just the simple name. + ImportRewrite importRewrite = ImportRewrite.create((CompilationUnit) node, true); + String Rqualifier = packageName + ".R"; //$NON-NLS-1$ + Rqualifier = importRewrite.addImport(Rqualifier); + + // Rewrite the AST itself via an ASTVisitor + AST ast = node.getAST(); + ASTRewrite astRewrite = ASTRewrite.create(ast); + ArrayList<TextEditGroup> astEditGroups = new ArrayList<TextEditGroup>(); + ReplaceStringsVisitor visitor = new ReplaceStringsVisitor( + ast, astRewrite, astEditGroups, + tokenString, Rqualifier, xmlStringId); + node.accept(visitor); + + // Finally prepare the change set + try { + MultiTextEdit edit = new MultiTextEdit(); + + // Create the edit to change the imports, only if anything changed + TextEdit subEdit = importRewrite.rewriteImports(monitor.newChild(1)); + if (subEdit.hasChildren()) { + edit.addChild(subEdit); + } + + // Create the edit to change the Java source, only if anything changed + subEdit = astRewrite.rewriteAST(); + if (subEdit.hasChildren()) { + edit.addChild(subEdit); + } + + // Only create a change set if any edit was collected + if (edit.hasChildren()) { + change.setEdit(edit); + + // Create TextEditChangeGroups which let the user turn changes on or off + // individually. This must be done after the change.setEdit() call above. + for (TextEditGroup editGroup : astEditGroups) { + TextEditChangeGroup group = new TextEditChangeGroup(change, editGroup); + if (editGroup instanceof EnabledTextEditGroup) { + group.setEnabled(((EnabledTextEditGroup) editGroup).isEnabled()); + } + change.addTextEditChangeGroup(group); + } + + changes.add(change); + } + + monitor.worked(1); + + if (changes.size() > 0) { + return changes; + } + + } catch (CoreException e) { + // ImportRewrite.rewriteImports failed. + status.addFatalError(e.getMessage()); + } + return null; + } + + // ---- + + /** + * Step 3 of 3 of the refactoring: returns the {@link Change} that will be able to do the + * work and creates a descriptor that can be used to replay that refactoring later. + * + * @see org.eclipse.ltk.core.refactoring.Refactoring#createChange(org.eclipse.core.runtime.IProgressMonitor) + * + * @throws CoreException + */ + @Override + public Change createChange(IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + + try { + monitor.beginTask("Applying changes...", 1); + + CompositeChange change = new CompositeChange( + getName(), + mChanges.toArray(new Change[mChanges.size()])) { + @Override + public ChangeDescriptor getDescriptor() { + + String comment = String.format( + "Extracts string '%1$s' into R.string.%2$s", + mTokenString, + mXmlStringId); + + ExtractStringDescriptor desc = new ExtractStringDescriptor( + mProject.getName(), //project + comment, //description + comment, //comment + createArgumentMap()); + + return new RefactoringChangeDescriptor(desc); + } + }; + + monitor.worked(1); + + return change; + + } finally { + monitor.done(); + } + + } + + /** + * Given a file project path, returns its resource in the same project than the + * compilation unit. The resource may not exist. + */ + private IResource getTargetXmlResource(String xmlFileWsPath) { + IResource resource = mProject.getFile(xmlFileWsPath); + return resource; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java new file mode 100644 index 000000000..556dff0df --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringWizard.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import org.eclipse.core.resources.IProject; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; + +/** + * A wizard for ExtractString based on a simple dialog with one page. + * + * @see ExtractStringInputPage + * @see ExtractStringRefactoring + */ +public class ExtractStringWizard extends RefactoringWizard { + + private final IProject mProject; + + /** + * Create a wizard for ExtractString based on a simple dialog with one page. + * + * @param ref The instance of {@link ExtractStringRefactoring} to associate to the wizard. + * @param project The project where the wizard was invoked from (e.g. where the user selection + * happened, so that we can retrieve project resources.) + */ + public ExtractStringWizard(ExtractStringRefactoring ref, IProject project) { + super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE); + mProject = project; + setDefaultPageTitle(ref.getName()); + } + + @Override + protected void addUserInputPages() { + addPage(new ExtractStringInputPage(mProject)); + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java new file mode 100644 index 000000000..e058ce1ba --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Assignment; +import org.eclipse.jdt.core.dom.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationExpression; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.VariableDeclarationStatement; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.text.edits.TextEditGroup; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +/** + * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing + * Java source and replace it by an Android XML string reference. + * + * @see ExtractStringRefactoring#computeJavaChanges + */ +class ReplaceStringsVisitor extends ASTVisitor { + + private static final String CLASS_ANDROID_CONTEXT = "android.content.Context"; //$NON-NLS-1$ + private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence"; //$NON-NLS-1$ + private static final String CLASS_JAVA_STRING = "java.lang.String"; //$NON-NLS-1$ + + + private final AST mAst; + private final ASTRewrite mRewriter; + private final String mOldString; + private final String mRQualifier; + private final String mXmlId; + private final ArrayList<TextEditGroup> mEditGroups; + + public ReplaceStringsVisitor(AST ast, + ASTRewrite astRewrite, + ArrayList<TextEditGroup> editGroups, + String oldString, + String rQualifier, + String xmlId) { + mAst = ast; + mRewriter = astRewrite; + mEditGroups = editGroups; + mOldString = oldString; + mRQualifier = rQualifier; + mXmlId = xmlId; + } + + @SuppressWarnings("unchecked") + @Override + public boolean visit(StringLiteral node) { + if (node.getLiteralValue().equals(mOldString)) { + + // We want to analyze the calling context to understand whether we can + // just replace the string literal by the named int constant (R.id.foo) + // or if we should generate a Context.getString() call. + boolean useGetResource = false; + useGetResource = examineVariableDeclaration(node) || + examineMethodInvocation(node) || + examineAssignment(node); + + Name qualifierName = mAst.newName(mRQualifier + ".string"); //$NON-NLS-1$ + SimpleName idName = mAst.newSimpleName(mXmlId); + ASTNode newNode = mAst.newQualifiedName(qualifierName, idName); + boolean disabledChange = false; + String title = "Replace string by ID"; + + if (useGetResource) { + Expression context = methodHasContextArgument(node); + if (context == null && !isClassDerivedFromContext(node)) { + // if we don't have a class that derives from Context and + // we don't have a Context method argument, then try a bit harder: + // can we find a method or a field that will give us a context? + context = findContextFieldOrMethod(node); + + if (context == null) { + // If not, let's write Context.getString(), which is technically + // invalid but makes it a good clue on how to fix it. Since these + // will not compile, we create a disabled change by default. + context = mAst.newSimpleName("Context"); //$NON-NLS-1$ + disabledChange = true; + } + } + + MethodInvocation mi2 = mAst.newMethodInvocation(); + mi2.setName(mAst.newSimpleName("getString")); //$NON-NLS-1$ + mi2.setExpression(context); + mi2.arguments().add(newNode); + + newNode = mi2; + title = "Replace string by Context.getString(R.string...)"; + } + + TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange); + mEditGroups.add(editGroup); + mRewriter.replace(node, newNode, editGroup); + } + return super.visit(node); + } + + /** + * Examines if the StringLiteral is part of an assignment corresponding to the + * a string variable declaration, e.g. String foo = id. + * + * The parent fragment is of syntax "var = expr" or "var[] = expr". + * We want the type of the variable, which is either held by a + * VariableDeclarationStatement ("type [fragment]") or by a + * VariableDeclarationExpression. In either case, the type can be an array + * but for us all that matters is to know whether the type is an int or + * a string. + */ + private boolean examineVariableDeclaration(StringLiteral node) { + VariableDeclarationFragment fragment = findParentClass(node, + VariableDeclarationFragment.class); + + if (fragment != null) { + ASTNode parent = fragment.getParent(); + + Type type = null; + if (parent instanceof VariableDeclarationStatement) { + type = ((VariableDeclarationStatement) parent).getType(); + } else if (parent instanceof VariableDeclarationExpression) { + type = ((VariableDeclarationExpression) parent).getType(); + } + + if (type instanceof SimpleType) { + return isJavaString(type.resolveBinding()); + } + } + + return false; + } + + /** + * Examines if the StringLiteral is part of a assignment to a variable that + * is a string. We need to lookup the variable to find its type, either in the + * enclosing method or class type. + */ + private boolean examineAssignment(StringLiteral node) { + + Assignment assignment = findParentClass(node, Assignment.class); + if (assignment != null) { + Expression left = assignment.getLeftHandSide(); + + ITypeBinding typeBinding = left.resolveTypeBinding(); + return isJavaString(typeBinding); + } + + return false; + } + + /** + * If the expression is part of a method invocation (aka a function call) or a + * class instance creation (aka a "new SomeClass" constructor call), we try to + * find the type of the argument being used. If it is a String (most likely), we + * want to return true (to generate a getString() call). However if there might + * be a similar method that takes an int, in which case we don't want to do that. + * + * This covers the case of Activity.setTitle(int resId) vs setTitle(String str). + */ + @SuppressWarnings("rawtypes") + private boolean examineMethodInvocation(StringLiteral node) { + + ASTNode parent = null; + List arguments = null; + IMethodBinding methodBinding = null; + + MethodInvocation invoke = findParentClass(node, MethodInvocation.class); + if (invoke != null) { + parent = invoke; + arguments = invoke.arguments(); + methodBinding = invoke.resolveMethodBinding(); + } else { + ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class); + if (newclass != null) { + parent = newclass; + arguments = newclass.arguments(); + methodBinding = newclass.resolveConstructorBinding(); + } + } + + if (parent != null && arguments != null && methodBinding != null) { + // We want to know which argument this is. + // Walk up the hierarchy again to find the immediate child of the parent, + // which should turn out to be one of the invocation arguments. + ASTNode child = null; + for (ASTNode n = node; n != parent; ) { + ASTNode p = n.getParent(); + if (p == parent) { + child = n; + break; + } + n = p; + } + if (child == null) { + // This can't happen: a parent of 'node' must be the child of 'parent'. + return false; + } + + // Find the index + int index = 0; + for (Object arg : arguments) { + if (arg == child) { + break; + } + index++; + } + + if (index == arguments.size()) { + // This can't happen: one of the arguments of 'invoke' must be 'child'. + return false; + } + + // Eventually we want to determine if the parameter is a string type, + // in which case a Context.getString() call must be generated. + boolean useStringType = false; + + // Find the type of that argument + ITypeBinding[] types = methodBinding.getParameterTypes(); + if (index < types.length) { + ITypeBinding type = types[index]; + useStringType = isJavaString(type); + } + + // Now that we know that this method takes a String parameter, can we find + // a variant that would accept an int for the same parameter position? + if (useStringType) { + String name = methodBinding.getName(); + ITypeBinding clazz = methodBinding.getDeclaringClass(); + nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) { + if (methodBinding == mb2 || !mb2.getName().equals(name)) { + continue; + } + // We found a method with the same name. We want the same parameters + // except that the one at 'index' must be an int type. + ITypeBinding[] types2 = mb2.getParameterTypes(); + int len2 = types2.length; + if (types.length == len2) { + for (int i = 0; i < len2; i++) { + if (i == index) { + ITypeBinding type2 = types2[i]; + if (!("int".equals(type2.getQualifiedName()))) { //$NON-NLS-1$ + // The argument at 'index' is not an int. + continue nextMethod; + } + } else if (!types[i].equals(types2[i])) { + // One of the other arguments do not match our original method + continue nextMethod; + } + } + // If we got here, we found a perfect match: a method with the same + // arguments except the one at 'index' is an int. In this case we + // don't need to convert our R.id into a string. + useStringType = false; + break; + } + } + } + + return useStringType; + } + return false; + } + + /** + * Examines if the StringLiteral is part of a method declaration (a.k.a. a function + * definition) which takes a Context argument. + * If such, it returns the name of the variable as a {@link SimpleName}. + * Otherwise it returns null. + */ + private SimpleName methodHasContextArgument(StringLiteral node) { + MethodDeclaration decl = findParentClass(node, MethodDeclaration.class); + if (decl != null) { + for (Object obj : decl.parameters()) { + if (obj instanceof SingleVariableDeclaration) { + SingleVariableDeclaration var = (SingleVariableDeclaration) obj; + if (isAndroidContext(var.getType())) { + return mAst.newSimpleName(var.getName().getIdentifier()); + } + } + } + } + return null; + } + + /** + * Walks up the node hierarchy to find the class (aka type) where this statement + * is used and returns true if this class derives from android.content.Context. + */ + private boolean isClassDerivedFromContext(StringLiteral node) { + TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); + if (clazz != null) { + // This is the class that the user is currently writing, so it can't be + // a Context by itself, it has to be derived from it. + return isAndroidContext(clazz.getSuperclassType()); + } + return false; + } + + private Expression findContextFieldOrMethod(StringLiteral node) { + TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class); + return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding()); + } + + private Expression findContextFieldOrMethod(ITypeBinding clazzType) { + TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>(); + findContextCandidates(results, clazzType, 0 /*superType*/); + if (results.size() > 0) { + Integer bestRating = results.keySet().iterator().next(); + return results.get(bestRating); + } + return null; + } + + /** + * Find all method or fields that are candidates for providing a Context. + * There can be various choices amongst this class or its super classes. + * Sort them by rating in the results map. + * + * The best ever choice is to find a method with no argument that returns a Context. + * The second suitable choice is to find a Context field. + * The least desirable choice is to find a method with arguments. It's not really + * desirable since we can't generate these arguments automatically. + * + * Methods and fields from supertypes are ignored if they are private. + * + * The rating is reversed: the lowest rating integer is used for the best candidate. + * Because the superType argument is actually a recursion index, this makes the most + * immediate classes more desirable. + * + * @param results The map that accumulates the rating=>expression results. The lower + * rating number is the best candidate. + * @param clazzType The class examined. + * @param superType The recursion index. + * 0 for the immediate class, 1 for its super class, etc. + */ + private void findContextCandidates(TreeMap<Integer, Expression> results, + ITypeBinding clazzType, + int superType) { + for (IMethodBinding mb : clazzType.getDeclaredMethods()) { + // If we're looking at supertypes, we can't use private methods. + if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) { + continue; + } + + if (isAndroidContext(mb.getReturnType())) { + // We found a method that returns something derived from Context. + + int argsLen = mb.getParameterTypes().length; + if (argsLen == 0) { + // We'll favor any method that takes no argument, + // That would be the best candidate ever, so we can stop here. + MethodInvocation mi = mAst.newMethodInvocation(); + mi.setName(mAst.newSimpleName(mb.getName())); + results.put(Integer.MIN_VALUE, mi); + return; + } else { + // A method with arguments isn't as interesting since we wouldn't + // know how to populate such arguments. We'll use it if there are + // no other alternatives. We'll favor the one with the less arguments. + Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen); + if (!results.containsKey(rating)) { + MethodInvocation mi = mAst.newMethodInvocation(); + mi.setName(mAst.newSimpleName(mb.getName())); + results.put(rating, mi); + } + } + } + } + + // A direct Context field would be more interesting than a method with + // arguments. Try to find one. + for (IVariableBinding var : clazzType.getDeclaredFields()) { + // If we're looking at supertypes, we can't use private field. + if (superType != 0 && Modifier.isPrivate(var.getModifiers())) { + continue; + } + + if (isAndroidContext(var.getType())) { + // We found such a field. Let's use it. + Integer rating = Integer.valueOf(superType); + results.put(rating, mAst.newSimpleName(var.getName())); + break; + } + } + + // Examine the super class to see if we can locate a better match + clazzType = clazzType.getSuperclass(); + if (clazzType != null) { + findContextCandidates(results, clazzType, superType + 1); + } + } + + /** + * Walks up the node hierarchy and returns the first ASTNode of the requested class. + * Only look at parents. + * + * Implementation note: this is a generic method so that it returns the node already + * casted to the requested type. + */ + @SuppressWarnings("unchecked") + private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) { + if (node != null) { + for (node = node.getParent(); node != null; node = node.getParent()) { + if (node.getClass().equals(clazz)) { + return (T) node; + } + } + } + return null; + } + + /** + * Returns true if the given type is or derives from android.content.Context. + */ + private boolean isAndroidContext(Type type) { + if (type != null) { + return isAndroidContext(type.resolveBinding()); + } + return false; + } + + /** + * Returns true if the given type is or derives from android.content.Context. + */ + private boolean isAndroidContext(ITypeBinding type) { + for (; type != null; type = type.getSuperclass()) { + if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) { + return true; + } + } + return false; + } + + /** + * Returns true if this type binding represents a String or CharSequence type. + */ + private boolean isJavaString(ITypeBinding type) { + for (; type != null; type = type.getSuperclass()) { + if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) || + CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) { + return true; + } + } + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java new file mode 100644 index 000000000..01e814ef2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/XmlStringFileHelper.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2009 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.extractstring; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +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.xml.core.internal.provisional.document.IDOMDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * An helper utility to get IDs out of an Android XML resource file. + */ +@SuppressWarnings("restriction") +class XmlStringFileHelper { + + /** A temporary cache of R.string IDs defined by a given xml file. The key is the + * project path of the file, the data is a set of known string Ids for that file. + * + * Map type: map [String filename] => map [String id => String value]. + */ + private HashMap<String, Map<String, String>> mResIdCache = + new HashMap<String, Map<String, String>>(); + + public XmlStringFileHelper() { + } + + /** + * Utility method used by the wizard to retrieve the actual value definition of a given + * string ID. + * + * @param project The project contain the XML file. + * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml". + * The given file may or may not exist. + * @param stringId The string ID to find. + * @return The value string if the ID is defined, null otherwise. + */ + public String valueOfStringId(IProject project, String xmlFileWsPath, String stringId) { + Map<String, String> cache = getResIdsForFile(project, xmlFileWsPath); + return cache.get(stringId); + } + + /** + * Utility method that retrieves all the *string* IDs defined in the given Android resource + * file. The instance maintains an internal cache so a given file is retrieved only once. + * Callers should consider the set to be read-only. + * + * @param project The project contain the XML file. + * @param xmlFileWsPath The project path of the XML file, e.g. "/res/values/strings.xml". + * The given file may or may not exist. + * @return The map of string IDs => values defined in the given file. Cached. Never null. + */ + public Map<String, String> getResIdsForFile(IProject project, String xmlFileWsPath) { + Map<String, String> cache = mResIdCache.get(xmlFileWsPath); + if (cache == null) { + cache = internalGetResIdsForFile(project, xmlFileWsPath); + mResIdCache.put(xmlFileWsPath, cache); + } + return cache; + } + + /** + * Extract all the defined string IDs from a given file using XPath. + * @param project The project contain the XML file. + * @param xmlFileWsPath The project path of the file to parse. It may not exist. + * @return The map of all string IDs => values defined in the file. + * The returned set is always non null. It is empty if the file does not exist. + */ + private Map<String, String> internalGetResIdsForFile(IProject project, String xmlFileWsPath) { + + TreeMap<String, String> ids = new TreeMap<String, String>(); + + // Access the project that contains the resource that contains the compilation unit + IResource resource = project.getFile(xmlFileWsPath); + + if (resource != null && resource.exists() && resource.getType() == IResource.FILE) { + IStructuredModel smodel = null; + + try { + IFile file = (IFile) resource; + IModelManager modelMan = StructuredModelManager.getModelManager(); + smodel = modelMan.getExistingModelForRead(file); + if (smodel == null) { + smodel = modelMan.getModelForRead(file); + } + + if (smodel instanceof IDOMModel) { + IDOMDocument doc = ((IDOMModel) smodel).getDocument(); + + // We want all the IDs in an XML structure like this: + // <resources> + // <string name="ID">something</string> + // </resources> + + Node root = findChild(doc, null, SdkConstants.TAG_RESOURCES); + if (root != null) { + for (Node strNode = findChild(root, null, + SdkConstants.TAG_STRING); + strNode != null; + strNode = findChild(null, strNode, + SdkConstants.TAG_STRING)) { + NamedNodeMap attrs = strNode.getAttributes(); + Node nameAttr = attrs.getNamedItem(SdkConstants.ATTR_NAME); + if (nameAttr != null) { + String id = nameAttr.getNodeValue(); + + // Find the TEXT node right after the element. + // Whitespace matters so we don't try to normalize it. + String text = ""; //$NON-NLS-1$ + for (Node txtNode = strNode.getFirstChild(); + txtNode != null && txtNode.getNodeType() == Node.TEXT_NODE; + txtNode = txtNode.getNextSibling()) { + text += txtNode.getNodeValue(); + } + + ids.put(id, text); + } + } + } + } + + } catch (Throwable e) { + AdtPlugin.log(e, "GetResIds failed in %1$s", xmlFileWsPath); //$NON-NLS-1$ + } finally { + if (smodel != null) { + smodel.releaseFromRead(); + } + } + } + + return ids; + } + + /** + * Utility method that finds the next node of the requested element name. + * + * @param parent The parent node. If not null, will to start searching its children. + * Set to null when iterating through children. + * @param lastChild The last child returned. Use null when visiting a parent the first time. + * @param elementName The element name of the node to find. + * @return The next children or sibling nide with the requested element name or null. + */ + private Node findChild(Node parent, Node lastChild, String elementName) { + if (lastChild == null && parent != null) { + lastChild = parent.getFirstChild(); + } else if (lastChild != null) { + lastChild = lastChild.getNextSibling(); + } + + for ( ; lastChild != null ; lastChild = lastChild.getNextSibling()) { + if (lastChild.getNodeType() == Node.ELEMENT_NODE && + lastChild.getNamespaceURI() == null && // resources don't have any NS URI + elementName.equals(lastChild.getLocalName())) { + return lastChild; + } + } + + return null; + } + +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java new file mode 100644 index 000000000..406cebca4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java @@ -0,0 +1,586 @@ +/* + * 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.renamepackage; + +import static com.android.SdkConstants.FN_BUILD_CONFIG_BASE; +import static com.android.SdkConstants.FN_MANIFEST_BASE; +import static com.android.SdkConstants.FN_RESOURCE_BASE; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceVisitor; +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.core.runtime.Status; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.TextEditGroup; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; +import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Wrapper class defining the stages of the refactoring process + */ +@SuppressWarnings("restriction") +class ApplicationPackageNameRefactoring extends Refactoring { + private final IProject mProject; + private final Name mOldPackageName; + private final Name mNewPackageName; + + List<String> MAIN_COMPONENT_TYPES_LIST = Arrays.asList(MAIN_COMPONENT_TYPES); + + ApplicationPackageNameRefactoring( + IProject project, + Name oldPackageName, + Name newPackageName) { + mProject = project; + mOldPackageName = oldPackageName; + mNewPackageName = newPackageName; + } + + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor pm) + throws CoreException, OperationCanceledException { + + // Accurate refactoring of the "shorthand" names in + // AndroidManifest.xml depends on not having compilation errors. + if (mProject.findMaxProblemSeverity( + IMarker.PROBLEM, + true, + IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) { + return + RefactoringStatus.createFatalErrorStatus("Fix the errors in your project, first."); + } + + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor pm) + throws OperationCanceledException { + + return new RefactoringStatus(); + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + + // Traverse all files in the project, building up a list of changes + JavaFileVisitor fileVisitor = new JavaFileVisitor(); + mProject.accept(fileVisitor); + return fileVisitor.getChange(); + } + + @Override + public String getName() { + return "AndroidPackageNameRefactoring"; //$NON-NLS-1$ + } + + public final static String[] MAIN_COMPONENT_TYPES = { + AndroidManifest.NODE_ACTIVITY, AndroidManifest.NODE_SERVICE, + AndroidManifest.NODE_RECEIVER, AndroidManifest.NODE_PROVIDER, + AndroidManifest.NODE_APPLICATION + }; + + + TextEdit updateJavaFileImports(CompilationUnit cu) { + + ImportVisitor importVisitor = new ImportVisitor(cu.getAST()); + cu.accept(importVisitor); + TextEdit rewrittenImports = importVisitor.getTextEdit(); + + // If the import of R was potentially implicit, insert an import statement + if (rewrittenImports != null && cu.getPackage().getName().getFullyQualifiedName() + .equals(mOldPackageName.getFullyQualifiedName())) { + + UsageVisitor usageVisitor = new UsageVisitor(); + cu.accept(usageVisitor); + + if (usageVisitor.seenAny()) { + ImportRewrite irw = ImportRewrite.create(cu, true); + if (usageVisitor.hasSeenR()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_RESOURCE_BASE); + } + if (usageVisitor.hasSeenBuildConfig()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_BUILD_CONFIG_BASE); + } + if (usageVisitor.hasSeenManifest()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_MANIFEST_BASE); + } + + try { + rewrittenImports.addChild( irw.rewriteImports(null) ); + } catch (MalformedTreeException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + } + } + + return rewrittenImports; + } + + // XML utility functions + private String stripQuotes(String text) { + int len = text.length(); + if (len >= 2 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') { + return text.substring(1, len - 1); + } else if (len >= 2 && text.charAt(0) == '\'' && text.charAt(len - 1) == '\'') { + return text.substring(1, len - 1); + } + return text; + } + + private String addQuotes(String text) { + return '"' + text + '"'; + } + + /* + * Make the appropriate package name changes to a resource file, + * e.g. .xml files in res/layout. This entails updating the namespace + * declarations for custom styleable attributes. The namespace prefix + * is user-defined and may be declared in any element where or parent + * element of where the prefix is used. + */ + TextFileChange editXmlResourceFile(IFile file) { + + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredDocument sdoc = null; + try { + sdoc = modelManager.createStructuredDocumentFor(file); + } catch (IOException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + + if (sdoc == null) { + return null; + } + + TextFileChange xmlChange = new TextFileChange("XML resource file edit", file); + xmlChange.setTextType(SdkConstants.EXT_XML); + + MultiTextEdit multiEdit = new MultiTextEdit(); + ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); + + final String oldAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, + mOldPackageName.getFullyQualifiedName()); + final String newAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, + mNewPackageName.getFullyQualifiedName()); + + // Prepare the change set + for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { + + if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + continue; + } + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String lastAttrName = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + // Memorize the last attribute name seen + lastAttrName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + // Check this is the attribute and the original string + + if (lastAttrName != null && + lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { + + String lastAttrValue = region.getText(subRegion); + if (oldAppNamespaceString.equals(stripQuotes(lastAttrValue))) { + + // Found an occurrence. Create a change for it. + TextEdit edit = new ReplaceEdit( + region.getStartOffset() + subRegion.getStart(), + subRegion.getTextLength(), + addQuotes(newAppNamespaceString)); + TextEditGroup editGroup = new TextEditGroup( + "Replace package name in custom namespace prefix", edit); + + multiEdit.addChild(edit); + editGroups.add(editGroup); + } + } + } + } + } + + if (multiEdit.hasChildren()) { + xmlChange.setEdit(multiEdit); + for (TextEditGroup group : editGroups) { + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); + } + + return xmlChange; + } + return null; + } + + /* + * Replace all instances of the package name in AndroidManifest.xml. + * This includes expanding shorthand paths for each Component (Activity, + * Service, etc.) and of course updating the application package name. + * The namespace prefix might not be "android", so we resolve it + * dynamically. + */ + TextFileChange editAndroidManifest(IFile file) { + + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredDocument sdoc = null; + try { + sdoc = modelManager.createStructuredDocumentFor(file); + } catch (IOException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + + if (sdoc == null) { + return null; + } + + TextFileChange xmlChange = new TextFileChange("Make Manifest edits", file); + xmlChange.setTextType(SdkConstants.EXT_XML); + + MultiTextEdit multiEdit = new MultiTextEdit(); + ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); + + // The namespace prefix is guaranteed to be resolved before + // the first use of this attribute + String android_name_attribute = null; + + // Prepare the change set + for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { + + // Only look at XML "top regions" + if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + continue; + } + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String lastTagName = null, lastAttrName = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_NAME.equals(type)) { + // Memorize the last tag name seen + lastTagName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + // Memorize the last attribute name seen + lastAttrName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + + String lastAttrValue = region.getText(subRegion); + if (lastAttrName != null && + lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { + + // Resolves the android namespace prefix for this file + if (SdkConstants.ANDROID_URI.equals(stripQuotes(lastAttrValue))) { + String android_namespace_prefix = lastAttrName + .substring(SdkConstants.XMLNS_PREFIX.length()); + android_name_attribute = android_namespace_prefix + ':' + + AndroidManifest.ATTRIBUTE_NAME; + } + } else if (AndroidManifest.NODE_MANIFEST.equals(lastTagName) + && AndroidManifest.ATTRIBUTE_PACKAGE.equals(lastAttrName)) { + + // Found an occurrence. Create a change for it. + TextEdit edit = new ReplaceEdit(region.getStartOffset() + + subRegion.getStart(), subRegion.getTextLength(), + addQuotes(mNewPackageName.getFullyQualifiedName())); + + multiEdit.addChild(edit); + editGroups.add(new TextEditGroup("Change Android package name", edit)); + + } else if (MAIN_COMPONENT_TYPES_LIST.contains(lastTagName) + && lastAttrName != null + && lastAttrName.equals(android_name_attribute)) { + + String package_path = stripQuotes(lastAttrValue); + String old_package_name_string = mOldPackageName.getFullyQualifiedName(); + + String absolute_path = AndroidManifest.combinePackageAndClassName( + old_package_name_string, package_path); + + TextEdit edit = new ReplaceEdit(region.getStartOffset() + + subRegion.getStart(), subRegion.getTextLength(), + addQuotes(absolute_path)); + + multiEdit.addChild(edit); + + editGroups.add(new TextEditGroup("Update component path", edit)); + } + } + } + } + + if (multiEdit.hasChildren()) { + xmlChange.setEdit(multiEdit); + for (TextEditGroup group : editGroups) { + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); + } + + return xmlChange; + } + return null; + } + + + /* + * Iterates through all project files, taking distinct actions based on + * whether the file is: + * 1) a .java file (replaces or inserts the "import" statements) + * 2) a .xml layout file (updates namespace declarations) + * 3) the AndroidManifest.xml + */ + class JavaFileVisitor implements IResourceVisitor { + + final List<TextFileChange> mChanges = new ArrayList<TextFileChange>(); + + final ASTParser mParser = ASTParser.newParser(AST.JLS3); + + public CompositeChange getChange() { + + Collections.reverse(mChanges); + CompositeChange change = new CompositeChange("Refactoring Application package name", + mChanges.toArray(new Change[mChanges.size()])); + change.markAsSynthetic(); + return change; + } + + @Override + public boolean visit(IResource resource) throws CoreException { + if (resource instanceof IFile) { + IFile file = (IFile) resource; + if (SdkConstants.EXT_JAVA.equals(file.getFileExtension())) { + + ICompilationUnit icu = JavaCore.createCompilationUnitFrom(file); + + mParser.setSource(icu); + CompilationUnit cu = (CompilationUnit) mParser.createAST(null); + + TextEdit textEdit = updateJavaFileImports(cu); + if (textEdit != null && textEdit.hasChildren()) { + MultiTextEdit edit = new MultiTextEdit(); + edit.addChild(textEdit); + + TextFileChange text_file_change = new TextFileChange(file.getName(), file); + text_file_change.setTextType(SdkConstants.EXT_JAVA); + text_file_change.setEdit(edit); + mChanges.add(text_file_change); + } + + // XXX Partially taken from ExtractStringRefactoring.java + // Check this a Layout XML file and get the selection and + // its context. + } else if (SdkConstants.EXT_XML.equals(file.getFileExtension())) { + + if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) { + // Ensure that this is the root manifest, not some other copy + // (such as the one in bin/) + IPath path = file.getFullPath(); + if (path.segmentCount() == 2) { + TextFileChange manifest_change = editAndroidManifest(file); + mChanges.add(manifest_change); + } + } else { + + // Currently we only support Android resource XML files, + // so they must have a path similar to + // project/res/<type>[-<configuration>]/*.xml + // There is no support for sub folders, so the segment count must be 4. + // We don't need to check the type folder name because + // a/ we only accept an AndroidXmlEditor source and + // b/ aapt generates a compilation error for unknown folders. + IPath path = file.getFullPath(); + // check if we are inside the project/res/* folder. + if (path.segmentCount() == 4) { + if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { + + + TextFileChange xmlChange = editXmlResourceFile(file); + if (xmlChange != null) { + mChanges.add(xmlChange); + } + } + } + } + } + + return false; + + } else if (resource instanceof IFolder) { + return !SdkConstants.FD_GEN_SOURCES.equals(resource.getName()); + } + + return true; + } + } + + private static class UsageVisitor extends ASTVisitor { + private boolean mSeenManifest; + private boolean mSeenR; + private boolean mSeenBuildConfig; + + @Override + public boolean visit(QualifiedName node) { + Name qualifier = node.getQualifier(); + if (qualifier.isSimpleName()) { + String name = qualifier.toString(); + if (name.equals(FN_RESOURCE_BASE)) { + mSeenR = true; + } else if (name.equals(FN_BUILD_CONFIG_BASE)) { + mSeenBuildConfig = true; + } else if (name.equals(FN_MANIFEST_BASE)) { + mSeenManifest = true; + } + } + return super.visit(node); + }; + + public boolean seenAny() { + return mSeenR || mSeenBuildConfig || mSeenManifest; + } + + public boolean hasSeenBuildConfig() { + return mSeenBuildConfig; + } + public boolean hasSeenManifest() { + return mSeenManifest; + } + public boolean hasSeenR() { + return mSeenR; + } + } + + private class ImportVisitor extends ASTVisitor { + + final AST mAst; + final ASTRewrite mRewriter; + + ImportVisitor(AST ast) { + mAst = ast; + mRewriter = ASTRewrite.create(ast); + } + + public TextEdit getTextEdit() { + try { + return this.mRewriter.rewriteAST(); + } catch (JavaModelException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (IllegalArgumentException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + return null; + } + + @Override + public boolean visit(ImportDeclaration id) { + + Name importName = id.getName(); + if (importName.isQualifiedName()) { + QualifiedName qualifiedImportName = (QualifiedName) importName; + + String identifier = qualifiedImportName.getName().getIdentifier(); + if (identifier.equals(FN_RESOURCE_BASE)) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + } else if (identifier.equals(FN_BUILD_CONFIG_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + + } else if (identifier.equals(FN_MANIFEST_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + } + } + + return true; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoringWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoringWizard.java new file mode 100644 index 000000000..3651855a7 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoringWizard.java @@ -0,0 +1,35 @@ +/* + * 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.renamepackage; + +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.ui.refactoring.RefactoringWizard; + +/** + * @see RenamePackageAction + */ +class ApplicationPackageNameRefactoringWizard extends RefactoringWizard { + + public ApplicationPackageNameRefactoringWizard(Refactoring refactoring) { + super(refactoring, 0); + } + + @Override + protected void addUserInputPages() { + } +} + diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java new file mode 100644 index 000000000..bb475aab1 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java @@ -0,0 +1,174 @@ +/* + * 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.renamepackage; + +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.window.Window; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.ui.IObjectActionDelegate; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; + +import java.util.Iterator; + +/** + * Refactoring steps: + * <ol> + * <li>Update the "package" attribute of the <manifest> tag with the new + * name.</li> + * <li>Replace all values for the "android:name" attribute in the + * <application> and "component class" (<activity>, <service>, + * <receiver>, and <provider>) tags with the non-shorthand version + * of the class name</li> + * <li>Replace package resource imports (*.R) in .java files</li> + * <li>Update package name in the namespace declarations (e.g. "xmlns:app") + * used for custom styleable attributes in layout resource files</li> + * </ol> + * Caveat: Sometimes it is necessary to perform a project-wide + * "Organize Imports" afterwards. (CTRL+SHIFT+O when a project has active + * selection) + */ +public class RenamePackageAction implements IObjectActionDelegate { + + private ISelection mSelection; + @SuppressWarnings("unused") private IWorkbenchPart mTargetPart; // TODO cleanup + + /** + * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart) + */ + @Override + public void setActivePart(IAction action, IWorkbenchPart targetPart) { + mTargetPart = targetPart; + } + + @Override + public void selectionChanged(IAction action, ISelection selection) { + mSelection = selection; + } + + /** + * @see IWorkbenchWindowActionDelegate#init + */ + public void init(IWorkbenchWindow window) { + // pass + } + + @Override + public void run(IAction action) { + + // Prompt for refactoring on the selected project + if (mSelection instanceof IStructuredSelection) { + for (Iterator<?> it = ((IStructuredSelection) mSelection).iterator(); it.hasNext();) { + Object element = it.next(); + IProject project = null; + if (element instanceof IProject) { + project = (IProject) element; + } else if (element instanceof IAdaptable) { + project = (IProject) ((IAdaptable) element).getAdapter(IProject.class); + } + if (project != null) { + // It is advisable that the user saves before proceeding, + // revealing any compilation errors. The following lines + // enforce a save as a convenience. + RefactoringSaveHelper save_helper = new RefactoringSaveHelper( + RefactoringSaveHelper.SAVE_ALL_ALWAYS_ASK); + if (save_helper.saveEditors(AdtPlugin.getShell())) { + promptNewName(project); + } + } + } + } + } + + /* + * Validate the new package name and start the refactoring wizard + */ + private void promptNewName(final IProject project) { + + ManifestData manifestData = AndroidManifestHelper.parseForData(project); + if (manifestData == null) { + return; + } + + final String oldPackageNameString = manifestData.getPackage(); + + final AST astValidator = AST.newAST(AST.JLS3); + Name oldPackageName = astValidator.newName(oldPackageNameString); + + IInputValidator validator = new IInputValidator() { + + @Override + public String isValid(String newText) { + try { + astValidator.newName(newText); + } catch (IllegalArgumentException e) { + return "Illegal package name."; + } + + if (newText.equals(oldPackageNameString)) + return "No change."; + else + return null; + } + }; + + InputDialog dialog = new InputDialog(AdtPlugin.getShell(), + "Rename Application Package", "Enter new package name:", oldPackageNameString, + validator); + + if (dialog.open() == Window.OK) { + Name newPackageName = astValidator.newName(dialog.getValue()); + initiateAndroidPackageRefactoring(project, oldPackageName, newPackageName); + } + } + + + private void initiateAndroidPackageRefactoring( + final IProject project, + Name oldPackageName, + Name newPackageName) { + + Refactoring package_name_refactoring = + new ApplicationPackageNameRefactoring(project, oldPackageName, newPackageName); + + ApplicationPackageNameRefactoringWizard wizard = + new ApplicationPackageNameRefactoringWizard(package_name_refactoring); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + op.run(AdtPlugin.getShell(), package_name_refactoring.getName()); + } catch (InterruptedException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + } +} |