diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java | 547 |
1 files changed, 547 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); + } + } + } +} |