Home | History | Annotate | Download | only in core
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.refactorings.core;
     18 
     19 import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
     20 import static com.android.SdkConstants.ANDROID_URI;
     21 import static com.android.SdkConstants.ATTR_CLASS;
     22 import static com.android.SdkConstants.ATTR_CONTEXT;
     23 import static com.android.SdkConstants.ATTR_NAME;
     24 import static com.android.SdkConstants.CLASS_VIEW;
     25 import static com.android.SdkConstants.DOT_XML;
     26 import static com.android.SdkConstants.EXT_XML;
     27 import static com.android.SdkConstants.R_CLASS;
     28 import static com.android.SdkConstants.TOOLS_URI;
     29 import static com.android.SdkConstants.VIEW_FRAGMENT;
     30 import static com.android.SdkConstants.VIEW_TAG;
     31 
     32 import com.android.SdkConstants;
     33 import com.android.annotations.NonNull;
     34 import com.android.ide.common.xml.ManifestData;
     35 import com.android.ide.eclipse.adt.AdtConstants;
     36 import com.android.ide.eclipse.adt.AdtPlugin;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     38 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     39 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
     40 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     42 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     43 import com.android.resources.ResourceFolderType;
     44 import com.android.resources.ResourceType;
     45 import com.android.utils.SdkUtils;
     46 
     47 import org.eclipse.core.resources.IFile;
     48 import org.eclipse.core.resources.IFolder;
     49 import org.eclipse.core.resources.IProject;
     50 import org.eclipse.core.resources.IResource;
     51 import org.eclipse.core.runtime.CoreException;
     52 import org.eclipse.core.runtime.IProgressMonitor;
     53 import org.eclipse.core.runtime.NullProgressMonitor;
     54 import org.eclipse.core.runtime.OperationCanceledException;
     55 import org.eclipse.jdt.core.IField;
     56 import org.eclipse.jdt.core.IJavaElement;
     57 import org.eclipse.jdt.core.IJavaProject;
     58 import org.eclipse.jdt.core.IType;
     59 import org.eclipse.jdt.core.ITypeHierarchy;
     60 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
     61 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
     62 import org.eclipse.ltk.core.refactoring.Change;
     63 import org.eclipse.ltk.core.refactoring.CompositeChange;
     64 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     65 import org.eclipse.ltk.core.refactoring.TextFileChange;
     66 import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
     67 import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
     68 import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
     69 import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
     70 import org.eclipse.text.edits.MultiTextEdit;
     71 import org.eclipse.text.edits.ReplaceEdit;
     72 import org.eclipse.text.edits.TextEdit;
     73 import org.eclipse.wst.sse.core.StructuredModelManager;
     74 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     75 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     76 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     77 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     78 import org.w3c.dom.Attr;
     79 import org.w3c.dom.Element;
     80 import org.w3c.dom.NamedNodeMap;
     81 import org.w3c.dom.Node;
     82 import org.w3c.dom.NodeList;
     83 
     84 import java.io.IOException;
     85 import java.util.ArrayList;
     86 import java.util.Collection;
     87 import java.util.List;
     88 
     89 /**
     90  * A participant to participate in refactorings that rename a type in an Android project.
     91  * The class updates android manifest and the layout file
     92  * The user can suppress refactoring by disabling the "Update references" checkbox.
     93  * <p>
     94  * Rename participants are registered via the extension point <code>
     95  * org.eclipse.ltk.core.refactoring.renameParticipants</code>.
     96  * Extensions to this extension point must therefore extend
     97  * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
     98  */
     99 @SuppressWarnings("restriction")
    100 public class AndroidTypeRenameParticipant extends RenameParticipant {
    101     private IProject mProject;
    102     private IFile mManifestFile;
    103     private String mOldFqcn;
    104     private String mNewFqcn;
    105     private String mOldSimpleName;
    106     private String mNewSimpleName;
    107     private String mOldDottedName;
    108     private String mNewDottedName;
    109     private boolean mIsCustomView;
    110 
    111     /**
    112      * Set while we are creating an embedded Java refactoring. This could cause a recursive
    113      * invocation of the XML renaming refactoring to react to the field, so this is flag
    114      * during the call to the Java processor, and is used to ignore requests for adding in
    115      * field reactions during that time.
    116      */
    117     private static boolean sIgnore;
    118 
    119     @Override
    120     public String getName() {
    121         return "Android Type Rename";
    122     }
    123 
    124     @Override
    125     public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
    126             throws OperationCanceledException {
    127         return new RefactoringStatus();
    128     }
    129 
    130     @Override
    131     protected boolean initialize(Object element) {
    132         if (sIgnore) {
    133             return false;
    134         }
    135 
    136         if (element instanceof IType) {
    137             IType type = (IType) element;
    138             IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT);
    139             mProject = javaProject.getProject();
    140             IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
    141                     + SdkConstants.FN_ANDROID_MANIFEST_XML);
    142 
    143             if (manifestResource == null || !manifestResource.exists()
    144                     || !(manifestResource instanceof IFile)) {
    145                 RefactoringUtil.logInfo(
    146                         String.format("Invalid or missing file %1$s in project %2$s",
    147                                 SdkConstants.FN_ANDROID_MANIFEST_XML,
    148                                 mProject.getName()));
    149                 return false;
    150             }
    151 
    152             try {
    153                 IType classView = javaProject.findType(CLASS_VIEW);
    154                 if (classView != null) {
    155                     ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
    156                     if (hierarchy.contains(classView)) {
    157                         mIsCustomView = true;
    158                     }
    159                 }
    160             } catch (CoreException e) {
    161                 AdtPlugin.log(e, null);
    162             }
    163 
    164             mManifestFile = (IFile) manifestResource;
    165             ManifestData manifestData;
    166             manifestData = AndroidManifestHelper.parseForData(mManifestFile);
    167             if (manifestData == null) {
    168                 return false;
    169             }
    170             mOldSimpleName = type.getElementName();
    171             mOldDottedName = '.' + mOldSimpleName;
    172             mOldFqcn = type.getFullyQualifiedName();
    173             String packageName = type.getPackageFragment().getElementName();
    174             mNewSimpleName = getArguments().getNewName();
    175             mNewDottedName = '.' + mNewSimpleName;
    176             if (packageName != null) {
    177                 mNewFqcn = packageName + mNewDottedName;
    178             } else {
    179                 mNewFqcn = mNewSimpleName;
    180             }
    181             if (mOldFqcn == null || mNewFqcn == null) {
    182                 return false;
    183             }
    184             if (!RefactoringUtil.isRefactorAppPackage() && mNewFqcn.indexOf('.') == -1) {
    185                 mNewFqcn = packageName + mNewDottedName;
    186             }
    187             return true;
    188         }
    189         return false;
    190     }
    191 
    192     @Override
    193     public Change createChange(IProgressMonitor pm) throws CoreException,
    194             OperationCanceledException {
    195         if (pm.isCanceled()) {
    196             return null;
    197         }
    198 
    199         // Only propose this refactoring if the "Update References" checkbox is set.
    200         if (!getArguments().getUpdateReferences()) {
    201             return null;
    202         }
    203 
    204         RefactoringProcessor p = getProcessor();
    205         if (p instanceof RenameCompilationUnitProcessor) {
    206             RenameTypeProcessor rtp =
    207                     ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
    208             if (rtp != null) {
    209                 String pattern = rtp.getFilePatterns();
    210                 boolean updQualf = rtp.getUpdateQualifiedNames();
    211                 if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
    212                     // Do not propose this refactoring if the
    213                     // "Update fully qualified names in non-Java files" option is
    214                     // checked and the file patterns mention XML. [c.f. SDK bug 21589]
    215                     return null;
    216                 }
    217             }
    218         }
    219 
    220         CompositeChange result = new CompositeChange(getName());
    221 
    222         // Only show the children in the refactoring preview dialog
    223         result.markAsSynthetic();
    224 
    225         addManifestFileChanges(mManifestFile, result);
    226         addLayoutFileChanges(mProject, result);
    227         addJavaChanges(mProject, result, pm);
    228 
    229         // Also update in dependent projects
    230         // TODO: Also do the Java elements, if they are in Jar files, since the library
    231         // projects do this (and the JDT refactoring does not include them)
    232         ProjectState projectState = Sdk.getProjectState(mProject);
    233         if (projectState != null) {
    234             Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
    235             for (ProjectState parentProject : parentProjects) {
    236                 IProject project = parentProject.getProject();
    237                 IResource manifestResource = project.findMember(AdtConstants.WS_SEP
    238                         + SdkConstants.FN_ANDROID_MANIFEST_XML);
    239                 if (manifestResource != null && manifestResource.exists()
    240                         && manifestResource instanceof IFile) {
    241                     addManifestFileChanges((IFile) manifestResource, result);
    242                 }
    243                 addLayoutFileChanges(project, result);
    244                 addJavaChanges(project, result, pm);
    245             }
    246         }
    247 
    248         // Look for the field change on the R.java class; it's a derived file
    249         // and will generate file modified manually warnings. Disable it.
    250         RenameResourceParticipant.disableRClassChanges(result);
    251 
    252         return (result.getChildren().length == 0) ? null : result;
    253     }
    254 
    255     private void addJavaChanges(IProject project, CompositeChange result, IProgressMonitor monitor) {
    256         if (!mIsCustomView) {
    257             return;
    258         }
    259 
    260         // Also rename styleables, if any
    261         try {
    262             // Find R class
    263             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
    264             ManifestInfo info = ManifestInfo.get(project);
    265             info.getPackage();
    266             String rFqcn = info.getPackage() + '.' + R_CLASS;
    267             IType styleable = javaProject.findType(rFqcn + '.' + ResourceType.STYLEABLE.getName());
    268             if (styleable != null) {
    269                 IField[] fields = styleable.getFields();
    270                 CompositeChange fieldChanges = null;
    271                 for (IField field : fields) {
    272                     String name = field.getElementName();
    273                     if (name.equals(mOldSimpleName) || name.startsWith(mOldSimpleName)
    274                             && name.length() > mOldSimpleName.length()
    275                             && name.charAt(mOldSimpleName.length()) == '_') {
    276                         // Rename styleable fields
    277                         String newName = name.equals(mOldSimpleName) ? mNewSimpleName :
    278                             mNewSimpleName + name.substring(mOldSimpleName.length());
    279                         RenameRefactoring refactoring =
    280                                 RenameResourceParticipant.createFieldRefactoring(field,
    281                                         newName, true);
    282 
    283                         try {
    284                             sIgnore = true;
    285                             RefactoringStatus status = refactoring.checkAllConditions(monitor);
    286                             if (status != null && !status.hasError()) {
    287                                 Change fieldChange = refactoring.createChange(monitor);
    288                                 if (fieldChange != null) {
    289                                     if (fieldChanges == null) {
    290                                         fieldChanges = new CompositeChange(
    291                                                 "Update custom view styleable fields");
    292                                         // Disable these changes. They sometimes end up
    293                                         // editing the wrong offsets. It looks like Eclipse
    294                                         // doesn't ensure that after applying each change it
    295                                         // also adjusts the other field offsets. I poked around
    296                                         // and couldn't find a way to do this properly, but
    297                                         // at least by listing the diffs here it shows what should
    298                                         // be done.
    299                                         fieldChanges.setEnabled(false);
    300                                     }
    301                                     // Disable change: see comment above.
    302                                     fieldChange.setEnabled(false);
    303                                     fieldChanges.add(fieldChange);
    304                                 }
    305                             }
    306                         } catch (CoreException e) {
    307                             AdtPlugin.log(e, null);
    308                         } finally {
    309                             sIgnore = false;
    310                         }
    311                     }
    312                 }
    313                 if (fieldChanges != null) {
    314                     result.add(fieldChanges);
    315                 }
    316             }
    317         } catch (CoreException e) {
    318             AdtPlugin.log(e, null);
    319         }
    320     }
    321 
    322     private void addManifestFileChanges(IFile manifestFile, CompositeChange result) {
    323         addXmlFileChanges(manifestFile, result, null);
    324     }
    325 
    326     private void addLayoutFileChanges(IProject project, CompositeChange result) {
    327         try {
    328             // Update references in XML resource files
    329             IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
    330 
    331             IResource[] folders = resFolder.members();
    332             for (IResource folder : folders) {
    333                 String folderName = folder.getName();
    334                 ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
    335                 if (folderType != ResourceFolderType.LAYOUT &&
    336                         folderType != ResourceFolderType.VALUES) {
    337                     continue;
    338                 }
    339                 if (!(folder instanceof IFolder)) {
    340                     continue;
    341                 }
    342                 IResource[] files = ((IFolder) folder).members();
    343                 for (int i = 0; i < files.length; i++) {
    344                     IResource member = files[i];
    345                     if ((member instanceof IFile) && member.exists()) {
    346                         IFile file = (IFile) member;
    347                         String fileName = member.getName();
    348 
    349                         if (SdkUtils.endsWith(fileName, DOT_XML)) {
    350                             addXmlFileChanges(file, result, folderType);
    351                         }
    352                     }
    353                 }
    354             }
    355         } catch (CoreException e) {
    356             RefactoringUtil.log(e);
    357         }
    358     }
    359 
    360     private boolean addXmlFileChanges(IFile file, CompositeChange changes,
    361             ResourceFolderType folderType) {
    362         IModelManager modelManager = StructuredModelManager.getModelManager();
    363         IStructuredModel model = null;
    364         try {
    365             model = modelManager.getExistingModelForRead(file);
    366             if (model == null) {
    367                 model = modelManager.getModelForRead(file);
    368             }
    369             if (model != null) {
    370                 IStructuredDocument document = model.getStructuredDocument();
    371                 if (model instanceof IDOMModel) {
    372                     IDOMModel domModel = (IDOMModel) model;
    373                     Element root = domModel.getDocument().getDocumentElement();
    374                     if (root != null) {
    375                         List<TextEdit> edits = new ArrayList<TextEdit>();
    376                         if (folderType == null) {
    377                             assert file.getName().equals(ANDROID_MANIFEST_XML);
    378                             addManifestReplacements(edits, root, document);
    379                         } else if (folderType == ResourceFolderType.VALUES) {
    380                             addValueReplacements(edits, root, document);
    381                         } else {
    382                             assert folderType == ResourceFolderType.LAYOUT;
    383                             addLayoutReplacements(edits, root, document);
    384                         }
    385                         if (!edits.isEmpty()) {
    386                             MultiTextEdit rootEdit = new MultiTextEdit();
    387                             rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
    388                             TextFileChange change = new TextFileChange(file.getName(), file);
    389                             change.setTextType(EXT_XML);
    390                             change.setEdit(rootEdit);
    391                             changes.add(change);
    392                         }
    393                     }
    394                 } else {
    395                     return false;
    396                 }
    397             }
    398 
    399             return true;
    400         } catch (IOException e) {
    401             AdtPlugin.log(e, null);
    402         } catch (CoreException e) {
    403             AdtPlugin.log(e, null);
    404         } finally {
    405             if (model != null) {
    406                 model.releaseFromRead();
    407             }
    408         }
    409 
    410         return false;
    411     }
    412 
    413     private void addLayoutReplacements(
    414             @NonNull List<TextEdit> edits,
    415             @NonNull Element element,
    416             @NonNull IStructuredDocument document) {
    417         String tag = element.getTagName();
    418         if (tag.equals(mOldFqcn)) {
    419             int start = RefactoringUtil.getTagNameRangeStart(element, document);
    420             if (start != -1) {
    421                 int end = start + mOldFqcn.length();
    422                 edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
    423             }
    424         } else if (tag.equals(VIEW_TAG)) {
    425             // TODO: Handle inner classes ($ vs .) ?
    426             Attr classNode = element.getAttributeNode(ATTR_CLASS);
    427             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
    428                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
    429                 if (start != -1) {
    430                     int end = start + mOldFqcn.length();
    431                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
    432                 }
    433             }
    434         } else if (tag.equals(VIEW_FRAGMENT)) {
    435             Attr classNode = element.getAttributeNode(ATTR_CLASS);
    436             if (classNode == null) {
    437                 classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
    438             }
    439             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
    440                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
    441                 if (start != -1) {
    442                     int end = start + mOldFqcn.length();
    443                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
    444                 }
    445             }
    446         } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
    447             Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
    448             if (classNode != null && classNode.getValue().equals(mOldFqcn)) {
    449                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
    450                 if (start != -1) {
    451                     int end = start + mOldFqcn.length();
    452                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
    453                 }
    454             } else if (classNode != null && classNode.getValue().equals(mOldDottedName)) {
    455                 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
    456                 if (start != -1) {
    457                     int end = start + mOldDottedName.length();
    458                     edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
    459                 }
    460             }
    461         }
    462 
    463         NodeList children = element.getChildNodes();
    464         for (int i = 0, n = children.getLength(); i < n; i++) {
    465             Node child = children.item(i);
    466             if (child.getNodeType() == Node.ELEMENT_NODE) {
    467                 addLayoutReplacements(edits, (Element) child, document);
    468             }
    469         }
    470     }
    471 
    472     private void addValueReplacements(
    473             @NonNull List<TextEdit> edits,
    474             @NonNull Element root,
    475             @NonNull IStructuredDocument document) {
    476         // Look for styleable renames for custom views
    477         String declareStyleable = ResourceType.DECLARE_STYLEABLE.getName();
    478         List<Element> topLevel = DomUtilities.getChildren(root);
    479         for (Element element : topLevel) {
    480             String tag = element.getTagName();
    481             if (declareStyleable.equals(tag)) {
    482                 Attr nameNode = element.getAttributeNode(ATTR_NAME);
    483                 if (nameNode != null && mOldSimpleName.equals(nameNode.getValue())) {
    484                     int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document);
    485                     if (start != -1) {
    486                         int end = start + mOldSimpleName.length();
    487                         edits.add(new ReplaceEdit(start, end - start, mNewSimpleName));
    488                     }
    489                 }
    490             }
    491         }
    492     }
    493 
    494     private void addManifestReplacements(
    495             @NonNull List<TextEdit> edits,
    496             @NonNull Element element,
    497             @NonNull IStructuredDocument document) {
    498         NamedNodeMap attributes = element.getAttributes();
    499         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    500             Attr attr = (Attr) attributes.item(i);
    501             if (!RefactoringUtil.isManifestClassAttribute(attr)) {
    502                 continue;
    503             }
    504 
    505             String value = attr.getValue();
    506             if (value.equals(mOldFqcn)) {
    507                 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    508                 if (start != -1) {
    509                     int end = start + mOldFqcn.length();
    510                     edits.add(new ReplaceEdit(start, end - start, mNewFqcn));
    511                 }
    512             } else if (value.equals(mOldDottedName)) {
    513                 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    514                 if (start != -1) {
    515                     int end = start + mOldDottedName.length();
    516                     edits.add(new ReplaceEdit(start, end - start, mNewDottedName));
    517                 }
    518             }
    519         }
    520 
    521         NodeList children = element.getChildNodes();
    522         for (int i = 0, n = children.getLength(); i < n; i++) {
    523             Node child = children.item(i);
    524             if (child.getNodeType() == Node.ELEMENT_NODE) {
    525                 addManifestReplacements(edits, (Element) child, document);
    526             }
    527         }
    528     }
    529 }