/* * 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 *

* Rename participants are registered via the extension point * org.eclipse.ltk.core.refactoring.renameParticipants. * Extensions to this extension point must therefore extend * org.eclipse.ltk.core.refactoring.participants.RenameParticipant. *

*/ @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 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 edits = new ArrayList(); 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 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 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); } } } }