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

* 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 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 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 edits = new ArrayList(); 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 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 edits, @NonNull Element root, @NonNull IStructuredDocument document) { // Look for styleable renames for custom views String declareStyleable = ResourceType.DECLARE_STYLEABLE.getName(); List 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 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); } } } }