| /* |
| * 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); |
| } |
| } |
| } |
| } |