Home | History | Annotate | Download | only in core
      1 /*
      2  * Copyright (C) 2012 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_PREFIX;
     20 import static com.android.SdkConstants.ANDROID_URI;
     21 import static com.android.SdkConstants.ATTR_ID;
     22 import static com.android.SdkConstants.ATTR_NAME;
     23 import static com.android.SdkConstants.ATTR_TYPE;
     24 import static com.android.SdkConstants.DOT_XML;
     25 import static com.android.SdkConstants.EXT_XML;
     26 import static com.android.SdkConstants.FD_RES;
     27 import static com.android.SdkConstants.FN_RESOURCE_CLASS;
     28 import static com.android.SdkConstants.NEW_ID_PREFIX;
     29 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     30 import static com.android.SdkConstants.PREFIX_THEME_REF;
     31 import static com.android.SdkConstants.R_CLASS;
     32 import static com.android.SdkConstants.TAG_ITEM;
     33 import static com.android.SdkConstants.TOOLS_URI;
     34 
     35 import com.android.SdkConstants;
     36 import com.android.annotations.NonNull;
     37 import com.android.annotations.Nullable;
     38 import com.android.ide.eclipse.adt.AdtPlugin;
     39 import com.android.ide.eclipse.adt.AdtUtils;
     40 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
     41 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
     42 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
     43 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     44 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     45 import com.android.resources.ResourceFolderType;
     46 import com.android.resources.ResourceType;
     47 import com.android.utils.SdkUtils;
     48 
     49 import org.eclipse.core.resources.IFile;
     50 import org.eclipse.core.resources.IFolder;
     51 import org.eclipse.core.resources.IProject;
     52 import org.eclipse.core.resources.IResource;
     53 import org.eclipse.core.runtime.CoreException;
     54 import org.eclipse.core.runtime.IPath;
     55 import org.eclipse.core.runtime.IProgressMonitor;
     56 import org.eclipse.core.runtime.OperationCanceledException;
     57 import org.eclipse.jdt.core.IField;
     58 import org.eclipse.jdt.core.IJavaElement;
     59 import org.eclipse.jdt.core.IJavaProject;
     60 import org.eclipse.jdt.core.IType;
     61 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor;
     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.TextChange;
     66 import org.eclipse.ltk.core.refactoring.TextFileChange;
     67 import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
     68 import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
     69 import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring;
     70 import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange;
     71 import org.eclipse.text.edits.MultiTextEdit;
     72 import org.eclipse.text.edits.ReplaceEdit;
     73 import org.eclipse.text.edits.TextEdit;
     74 import org.eclipse.wst.sse.core.StructuredModelManager;
     75 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     76 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     77 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     78 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     79 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     80 import org.w3c.dom.Attr;
     81 import org.w3c.dom.Element;
     82 import org.w3c.dom.NamedNodeMap;
     83 import org.w3c.dom.Node;
     84 import org.w3c.dom.NodeList;
     85 
     86 import java.io.IOException;
     87 import java.util.ArrayList;
     88 import java.util.List;
     89 
     90 /**
     91  * A rename participant handling renames of resources (such as R.id.foo and R.layout.bar).
     92  * This reacts to refactorings of fields in the R inner classes (such as R.id), and updates
     93  * the XML files as appropriate; renaming .xml files, updating XML attributes, resource
     94  * references in style declarations, and so on.
     95  */
     96 @SuppressWarnings("restriction") // WTP API
     97 public class RenameResourceParticipant extends RenameParticipant {
     98     /** The project we're refactoring in */
     99     private @NonNull IProject mProject;
    100 
    101     /** The type of the resource we're refactoring, such as {@link ResourceType#ID} */
    102     private @NonNull ResourceType mType;
    103     /**
    104      * The type of the resource folder we're refactoring in, such as
    105      * {@link ResourceFolderType#VALUES}. When refactoring non value files, we need to
    106      * rename the files as well.
    107      */
    108     private @NonNull ResourceFolderType mFolderType;
    109 
    110     /** The previous name of the resource */
    111     private @NonNull String mOldName;
    112 
    113     /** The new name of the resource */
    114     private @NonNull String mNewName;
    115 
    116     /** Whether references to the resource should be updated */
    117     private boolean mUpdateReferences;
    118 
    119     /** A match pattern to look for in XML, such as {@code @attr/foo} */
    120     private @NonNull String mXmlMatch1;
    121 
    122     /** A match pattern to look for in XML, such as {@code ?attr/foo} */
    123     private @Nullable String mXmlMatch2;
    124 
    125     /** A match pattern to look for in XML, such as {@code ?foo} */
    126     private @Nullable String mXmlMatch3;
    127 
    128     /** The value to replace a reference to {@link #mXmlMatch1} with, such as {@code @attr/bar} */
    129     private @NonNull String mXmlNewValue1;
    130 
    131     /** The value to replace a reference to {@link #mXmlMatch2} with, such as {@code ?attr/bar} */
    132     private @Nullable String mXmlNewValue2;
    133 
    134     /** The value to replace a reference to {@link #mXmlMatch3} with, such as {@code ?bar} */
    135     private @Nullable String mXmlNewValue3;
    136 
    137     /**
    138      * If non null, this refactoring was initiated as a file rename of an XML file (and if
    139      * null, we are just reacting to a Java field rename)
    140      */
    141     private IFile mRenamedFile;
    142 
    143     /**
    144      * If renaming a field, we need to create an embedded field refactoring to update the
    145      * Java sources referring to the corresponding R class field. This is stored as an
    146      * instance such that we can have it participate in both the condition check methods
    147      * as well as the {@link #createChange(IProgressMonitor)} refactoring operation.
    148      */
    149     private RenameRefactoring mFieldRefactoring;
    150 
    151     /**
    152      * Set while we are creating an embedded Java refactoring. This could cause a recursive
    153      * invocation of the XML renaming refactoring to react to the field, so this is flag
    154      * during the call to the Java processor, and is used to ignore requests for adding in
    155      * field reactions during that time.
    156      */
    157     private static boolean sIgnore;
    158 
    159     /**
    160      * Creates a new {@linkplain RenameResourceParticipant}
    161      */
    162     public RenameResourceParticipant() {
    163     }
    164 
    165     @Override
    166     public String getName() {
    167         return "Android Rename Field Participant";
    168     }
    169 
    170     @Override
    171     protected boolean initialize(Object element) {
    172         if (sIgnore) {
    173             return false;
    174         }
    175 
    176         if (element instanceof IField) {
    177             IField field = (IField) element;
    178             IType declaringType = field.getDeclaringType();
    179             if (declaringType != null) {
    180                 if (R_CLASS.equals(declaringType.getParent().getElementName())) {
    181                     String typeName = declaringType.getElementName();
    182                     mType = ResourceType.getEnum(typeName);
    183                     if (mType != null) {
    184                         mUpdateReferences = getArguments().getUpdateReferences();
    185                         mFolderType = AdtUtils.getFolderTypeFor(mType);
    186                         IJavaProject javaProject = (IJavaProject) field.getAncestor(
    187                                 IJavaElement.JAVA_PROJECT);
    188                         mProject = javaProject.getProject();
    189                         mOldName = field.getElementName();
    190                         mNewName = getArguments().getNewName();
    191                         mFieldRefactoring = null;
    192                         mRenamedFile = null;
    193                         createXmlSearchPatterns();
    194                         return true;
    195                     }
    196                 }
    197             }
    198 
    199             return false;
    200         } else if (element instanceof IFile) {
    201             IFile file = (IFile) element;
    202             mProject = file.getProject();
    203             if (BaseProjectHelper.isAndroidProject(mProject)) {
    204                 IPath path = file.getFullPath();
    205                 int segments = path.segmentCount();
    206                 if (segments == 4 && path.segment(1).equals(FD_RES)) {
    207                     String parentName = file.getParent().getName();
    208                     mFolderType = ResourceFolderType.getFolderType(parentName);
    209                     if (mFolderType != null && mFolderType != ResourceFolderType.VALUES) {
    210                         mType = AdtUtils.getResourceTypeFor(mFolderType);
    211                         if (mType != null) {
    212                             mUpdateReferences = getArguments().getUpdateReferences();
    213                             mProject = file.getProject();
    214                             mOldName = AdtUtils.stripAllExtensions(file.getName());
    215                             mNewName = AdtUtils.stripAllExtensions(getArguments().getNewName());
    216                             mRenamedFile = file;
    217                             createXmlSearchPatterns();
    218 
    219                             mFieldRefactoring = null;
    220                             IField field = getResourceField(mProject, mType, mOldName);
    221                             if (field != null) {
    222                                 mFieldRefactoring = createFieldRefactoring(field);
    223                             } else {
    224                                 // no corresponding field; aapt has not run yet. Perhaps user has
    225                                 // turned off auto build.
    226                                 mFieldRefactoring = null;
    227                             }
    228 
    229                             return true;
    230                         }
    231                     }
    232                 }
    233             }
    234         } else if (element instanceof String) {
    235             String uri = (String) element;
    236             if (uri.startsWith(PREFIX_RESOURCE_REF) && !uri.startsWith(ANDROID_PREFIX)) {
    237                 RenameResourceProcessor processor = (RenameResourceProcessor) getProcessor();
    238                 mProject = processor.getProject();
    239                 mType = processor.getType();
    240                 mFolderType = AdtUtils.getFolderTypeFor(mType);
    241                 mOldName = processor.getCurrentName();
    242                 mNewName = processor.getNewName();
    243                 assert uri.endsWith(mOldName) && uri.contains(mType.getName()) : uri;
    244                 mUpdateReferences = getArguments().getUpdateReferences();
    245                 if (mNewName.isEmpty()) {
    246                     mUpdateReferences = false;
    247                 }
    248                 mRenamedFile = null;
    249                 createXmlSearchPatterns();
    250                 mFieldRefactoring = null;
    251                 if (!mNewName.isEmpty()) {
    252                     IField field = getResourceField(mProject, mType, mOldName);
    253                     if (field != null) {
    254                         mFieldRefactoring = createFieldRefactoring(field);
    255                     }
    256                 }
    257 
    258                 return true;
    259             }
    260         }
    261 
    262         return false;
    263     }
    264 
    265     /** Create nested Java refactoring which updates the R field references, if applicable */
    266     private RenameRefactoring createFieldRefactoring(IField field) {
    267         return createFieldRefactoring(field, mNewName, mUpdateReferences);
    268     }
    269 
    270     /**
    271      * Create nested Java refactoring which updates the R field references, if
    272      * applicable
    273      *
    274      * @param field the field to be refactored
    275      * @param newName the new name
    276      * @param updateReferences whether references should be updated
    277      * @return a new rename refactoring
    278      */
    279     public static RenameRefactoring createFieldRefactoring(
    280             @NonNull IField field,
    281             @NonNull String newName,
    282             boolean updateReferences) {
    283         RenameFieldProcessor processor = new RenameFieldProcessor(field);
    284         processor.setRenameGetter(false);
    285         processor.setRenameSetter(false);
    286         RenameRefactoring refactoring = new RenameRefactoring(processor);
    287         processor.setUpdateReferences(updateReferences);
    288         processor.setUpdateTextualMatches(false);
    289         processor.setNewElementName(newName);
    290         try {
    291             if (refactoring.isApplicable()) {
    292                 return refactoring;
    293             }
    294         } catch (CoreException e) {
    295             AdtPlugin.log(e, null);
    296         }
    297 
    298         return null;
    299     }
    300 
    301     private void createXmlSearchPatterns() {
    302         // Set up search strings for the attribute iterator. This will
    303         // identify string matches for mXmlMatch1, 2 and 3, and when matched,
    304         // will add a replacement edit for mXmlNewValue1, 2, or 3.
    305         mXmlMatch2 = null;
    306         mXmlNewValue2 = null;
    307         mXmlMatch3 = null;
    308         mXmlNewValue3 = null;
    309 
    310         String typeName = mType.getName();
    311         if (mUpdateReferences) {
    312             mXmlMatch1 = PREFIX_RESOURCE_REF + typeName + '/' + mOldName;
    313             mXmlNewValue1 = PREFIX_RESOURCE_REF + typeName + '/' + mNewName;
    314             if (mType == ResourceType.ID) {
    315                 mXmlMatch2 = NEW_ID_PREFIX + mOldName;
    316                 mXmlNewValue2 = NEW_ID_PREFIX + mNewName;
    317             } else if (mType == ResourceType.ATTR) {
    318                 // When renaming @attr/foo, also edit ?attr/foo
    319                 mXmlMatch2 = PREFIX_THEME_REF + typeName + '/' + mOldName;
    320                 mXmlNewValue2 = PREFIX_THEME_REF + typeName + '/' + mNewName;
    321                 // as well as ?foo
    322                 mXmlMatch3 = PREFIX_THEME_REF + mOldName;
    323                 mXmlNewValue3 = PREFIX_THEME_REF + mNewName;
    324             }
    325         } else if (mType == ResourceType.ID) {
    326             mXmlMatch1 = NEW_ID_PREFIX + mOldName;
    327             mXmlNewValue1 = NEW_ID_PREFIX + mNewName;
    328         }
    329     }
    330 
    331     @Override
    332     public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
    333             throws OperationCanceledException {
    334         if (mRenamedFile != null && getArguments().getNewName().indexOf('.') == -1
    335                 && mRenamedFile.getName().indexOf('.') != -1) {
    336             return RefactoringStatus.createErrorStatus(
    337                     String.format("You must include the file extension (%1$s?)",
    338                            mRenamedFile.getName().substring(mRenamedFile.getName().indexOf('.'))));
    339         }
    340 
    341         // Ensure that the new name is valid
    342         if (mNewName != null && !mNewName.isEmpty()) {
    343             ResourceNameValidator validator = ResourceNameValidator.create(false, mProject, mType);
    344             String error = validator.isValid(mNewName);
    345             if (error != null) {
    346                 return RefactoringStatus.createErrorStatus(error);
    347             }
    348         }
    349 
    350         if (mFieldRefactoring != null) {
    351             try {
    352                 sIgnore = true;
    353                 return mFieldRefactoring.checkAllConditions(pm);
    354             } catch (CoreException e) {
    355                 AdtPlugin.log(e, null);
    356             } finally {
    357                 sIgnore = false;
    358             }
    359         }
    360 
    361         return new RefactoringStatus();
    362     }
    363 
    364     @Override
    365     public Change createChange(IProgressMonitor monitor) throws CoreException,
    366             OperationCanceledException {
    367         if (monitor.isCanceled()) {
    368             return null;
    369         }
    370 
    371         CompositeChange result = new CompositeChange("Update resource references");
    372 
    373         // Only show the children in the refactoring preview dialog
    374         result.markAsSynthetic();
    375 
    376         addResourceFileChanges(result, mProject, monitor);
    377 
    378         // If renaming resources in a library project, also offer to rename references
    379         // in including projects
    380         if (mUpdateReferences) {
    381             ProjectState projectState = Sdk.getProjectState(mProject);
    382             if (projectState != null && projectState.isLibrary()) {
    383                 List<ProjectState> parentProjects = projectState.getParentProjects();
    384                 for (ProjectState state : parentProjects) {
    385                     IProject project = state.getProject();
    386                     CompositeChange nested = new CompositeChange(
    387                             String.format("Update references in %1$s", project.getName()));
    388                     addResourceFileChanges(nested, project, monitor);
    389                     if (nested.getChildren().length > 0) {
    390                         result.add(nested);
    391                     }
    392                 }
    393             }
    394         }
    395 
    396         if (mFieldRefactoring != null) {
    397             // We have to add in Java field refactoring
    398             try {
    399                 sIgnore = true;
    400                 addJavaChanges(result, monitor);
    401             } finally {
    402                 sIgnore = false;
    403             }
    404         } else {
    405             // Disable field refactoring added by the default Java field rename handler
    406             disableExistingResourceFileChange();
    407         }
    408 
    409         return (result.getChildren().length == 0) ? null : result;
    410     }
    411 
    412     /**
    413      * Adds all changes to resource files (typically XML but also renaming drawable files
    414      *
    415      * @param project the Android project
    416      * @param className the layout classes
    417      */
    418     private void addResourceFileChanges(
    419             CompositeChange change,
    420             IProject project,
    421             IProgressMonitor monitor)
    422             throws OperationCanceledException {
    423         if (monitor.isCanceled()) {
    424             return;
    425         }
    426 
    427         try {
    428             // Update resource references in the manifest
    429             IFile manifest = project.getFile(SdkConstants.ANDROID_MANIFEST_XML);
    430             if (manifest != null) {
    431                 addResourceXmlChanges(manifest, change, null);
    432             }
    433 
    434             // Update references in XML resource files
    435             IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
    436 
    437             IResource[] folders = resFolder.members();
    438             for (IResource folder : folders) {
    439                 if (!(folder instanceof IFolder)) {
    440                     continue;
    441                 }
    442                 String folderName = folder.getName();
    443                 ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
    444                 IResource[] files = ((IFolder) folder).members();
    445                 for (int i = 0; i < files.length; i++) {
    446                     IResource member = files[i];
    447                     if ((member instanceof IFile) && member.exists()) {
    448                         IFile file = (IFile) member;
    449                         String fileName = member.getName();
    450 
    451                         if (SdkUtils.endsWith(fileName, DOT_XML)) {
    452                             addResourceXmlChanges(file, change, folderType);
    453                         }
    454 
    455                         if ((mRenamedFile == null || !mRenamedFile.equals(file))
    456                                 && fileName.startsWith(mOldName)
    457                                 && fileName.length() > mOldName.length()
    458                                 && fileName.charAt(mOldName.length()) == '.'
    459                                 && mFolderType != ResourceFolderType.VALUES
    460                                 && mFolderType == folderType) {
    461                             // Rename this file
    462                             String newFile = mNewName + fileName.substring(mOldName.length());
    463                             IPath path = file.getFullPath();
    464                             change.add(new RenameResourceChange(path, newFile));
    465                         }
    466                     }
    467                 }
    468             }
    469         } catch (CoreException e) {
    470             RefactoringUtil.log(e);
    471         }
    472     }
    473 
    474     private void addJavaChanges(CompositeChange result, IProgressMonitor monitor)
    475             throws CoreException, OperationCanceledException {
    476         if (monitor.isCanceled()) {
    477             return;
    478         }
    479 
    480         RefactoringStatus status = mFieldRefactoring.checkAllConditions(monitor);
    481         if (status != null && !status.hasError()) {
    482             Change fieldChanges = mFieldRefactoring.createChange(monitor);
    483             if (fieldChanges != null) {
    484                 result.add(fieldChanges);
    485 
    486                 // Look for the field change on the R.java class; it's a derived file
    487                 // and will generate file modified manually warnings. Disable it.
    488                 disableRClassChanges(fieldChanges);
    489             }
    490         }
    491     }
    492 
    493     private boolean addResourceXmlChanges(
    494             IFile file,
    495             CompositeChange changes,
    496             ResourceFolderType folderType) {
    497         IModelManager modelManager = StructuredModelManager.getModelManager();
    498         IStructuredModel model = null;
    499         try {
    500             model = modelManager.getExistingModelForRead(file);
    501             if (model == null) {
    502                 model = modelManager.getModelForRead(file);
    503             }
    504             if (model != null) {
    505                 IStructuredDocument document = model.getStructuredDocument();
    506                 if (model instanceof IDOMModel) {
    507                     IDOMModel domModel = (IDOMModel) model;
    508                     Element root = domModel.getDocument().getDocumentElement();
    509                     if (root != null) {
    510                         List<TextEdit> edits = new ArrayList<TextEdit>();
    511                         addReplacements(edits, root, document, folderType);
    512                         if (!edits.isEmpty()) {
    513                             MultiTextEdit rootEdit = new MultiTextEdit();
    514                             rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
    515                             TextFileChange change = new TextFileChange(file.getName(), file);
    516                             change.setTextType(EXT_XML);
    517                             change.setEdit(rootEdit);
    518                             changes.add(change);
    519                         }
    520                     }
    521                 } else {
    522                     return false;
    523                 }
    524             }
    525 
    526             return true;
    527         } catch (IOException e) {
    528             AdtPlugin.log(e, null);
    529         } catch (CoreException e) {
    530             AdtPlugin.log(e, null);
    531         } finally {
    532             if (model != null) {
    533                 model.releaseFromRead();
    534             }
    535         }
    536 
    537         return false;
    538     }
    539 
    540     private void addReplacements(
    541             @NonNull List<TextEdit> edits,
    542             @NonNull Element element,
    543             @NonNull IStructuredDocument document,
    544             @Nullable ResourceFolderType folderType) {
    545         String tag = element.getTagName();
    546         if (folderType == ResourceFolderType.VALUES) {
    547             // Look for
    548             //   <item name="main_layout" type="layout">...</item>
    549             //   <item name="myid" type="id"/>
    550             //   <string name="mystring">...</string>
    551             // etc
    552             if (tag.equals(mType.getName())
    553                     || (tag.equals(TAG_ITEM)
    554                             && (mType == ResourceType.ID
    555                                 || mType.getName().equals(element.getAttribute(ATTR_TYPE))))) {
    556                 Attr nameNode = element.getAttributeNode(ATTR_NAME);
    557                 if (nameNode != null && nameNode.getValue().equals(mOldName)) {
    558                     int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document);
    559                     if (start != -1) {
    560                         int end = start + mOldName.length();
    561                         edits.add(new ReplaceEdit(start, end - start, mNewName));
    562                     }
    563                 }
    564             }
    565         }
    566 
    567         NamedNodeMap attributes = element.getAttributes();
    568         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    569             Attr attr = (Attr) attributes.item(i);
    570             String value = attr.getValue();
    571 
    572             // If not updating references, only update XML matches that define the id
    573             if (!mUpdateReferences && (!ATTR_ID.equals(attr.getLocalName()) ||
    574                     !ANDROID_URI.equals(attr.getNamespaceURI()))) {
    575 
    576                 if (TOOLS_URI.equals(attr.getNamespaceURI()) && value.equals(mXmlMatch1)) {
    577                     int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    578                     if (start != -1) {
    579                         int end = start + mXmlMatch1.length();
    580                         edits.add(new ReplaceEdit(start, end - start, mXmlNewValue1));
    581                     }
    582                 }
    583 
    584                 continue;
    585             }
    586 
    587             // Replace XML attribute reference, such as
    588             //   android:id="@+id/oldName"   =>   android:id="+id/newName"
    589 
    590             String match = null;
    591             String matchedValue = null;
    592 
    593             if (value.equals(mXmlMatch1)) {
    594                 match = mXmlMatch1;
    595                 matchedValue = mXmlNewValue1;
    596             } else if (value.equals(mXmlMatch2)) {
    597                 match = mXmlMatch2;
    598                 matchedValue = mXmlNewValue2;
    599             } else if (value.equals(mXmlMatch3)) {
    600                 match = mXmlMatch3;
    601                 matchedValue = mXmlNewValue3;
    602             } else {
    603                 continue;
    604             }
    605 
    606             if (match != null) {
    607                 if (mNewName.isEmpty() && ATTR_ID.equals(attr.getLocalName()) &&
    608                         ANDROID_URI.equals(attr.getNamespaceURI())) {
    609                     // Delete attribute
    610                     IndexedRegion region = (IndexedRegion) attr;
    611                     int start = region.getStartOffset();
    612                     int end = region.getEndOffset();
    613                     edits.add(new ReplaceEdit(start, end - start, ""));
    614                 } else {
    615                     int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    616                     if (start != -1) {
    617                         int end = start + match.length();
    618                         edits.add(new ReplaceEdit(start, end - start, matchedValue));
    619                     }
    620                 }
    621             }
    622         }
    623 
    624         NodeList children = element.getChildNodes();
    625         for (int i = 0, n = children.getLength(); i < n; i++) {
    626             Node child = children.item(i);
    627             if (child.getNodeType() == Node.ELEMENT_NODE) {
    628                 addReplacements(edits, (Element) child, document, folderType);
    629             } else if (child.getNodeType() == Node.TEXT_NODE && mUpdateReferences) {
    630                 // Replace XML text, such as @color/custom_theme_color in
    631                 //    <item name="android:windowBackground">@color/custom_theme_color</item>
    632                 //
    633                 String text = child.getNodeValue();
    634                 int index = getFirstNonBlankIndex(text);
    635                 if (index != -1) {
    636                     String match = null;
    637                     String matchedValue = null;
    638                     if (mXmlMatch1 != null
    639                             && text.startsWith(mXmlMatch1) && text.trim().equals(mXmlMatch1)) {
    640                         match = mXmlMatch1;
    641                         matchedValue = mXmlNewValue1;
    642                     } else if (mXmlMatch2 != null
    643                             && text.startsWith(mXmlMatch2) && text.trim().equals(mXmlMatch2)) {
    644                         match = mXmlMatch2;
    645                         matchedValue = mXmlNewValue2;
    646                     } else if (mXmlMatch3 != null
    647                             && text.startsWith(mXmlMatch3) && text.trim().equals(mXmlMatch3)) {
    648                         match = mXmlMatch3;
    649                         matchedValue = mXmlNewValue3;
    650                     }
    651                     if (match != null) {
    652                         IndexedRegion region = (IndexedRegion) child;
    653                         int start = region.getStartOffset() + index;
    654                         int end = start + match.length();
    655                         edits.add(new ReplaceEdit(start, end - start, matchedValue));
    656                     }
    657                 }
    658             }
    659         }
    660     }
    661 
    662     /**
    663      * Returns the index of the first non-space character in the string, or -1
    664      * if the string is empty or has only whitespace
    665      *
    666      * @param s the string to check
    667      * @return the index of the first non whitespace character
    668      */
    669     private int getFirstNonBlankIndex(String s) {
    670         for (int i = 0, n = s.length(); i < n; i++) {
    671             if (!Character.isWhitespace(s.charAt(i))) {
    672                 return i;
    673             }
    674         }
    675 
    676         return -1;
    677     }
    678 
    679     /**
    680      * Initiates a renaming of a resource item
    681      *
    682      * @param project the project containing the resource references
    683      * @param type the type of resource
    684      * @param name the name of the resource
    685      * @return false if initiating the rename failed
    686      */
    687     @Nullable
    688     private static IField getResourceField(
    689             @NonNull IProject project,
    690             @NonNull ResourceType type,
    691             @NonNull String name) {
    692         try {
    693             IJavaProject javaProject = BaseProjectHelper.getJavaProject(project);
    694             if (javaProject == null) {
    695                 return null;
    696             }
    697 
    698             String pkg = ManifestInfo.get(project).getPackage();
    699             // TODO: Rename in all libraries too?
    700             IType t = javaProject.findType(pkg + '.' + R_CLASS + '.' + type.getName());
    701             if (t == null) {
    702                 return null;
    703             }
    704 
    705             return t.getField(name);
    706         } catch (CoreException e) {
    707             AdtPlugin.log(e, null);
    708         }
    709 
    710         return null;
    711     }
    712 
    713     /**
    714      * Searches for existing changes in the refactoring which modifies the R
    715      * field to rename it. it's derived so performing this change will generate
    716      * a "generated code was modified manually" warning
    717      */
    718     private void disableExistingResourceFileChange() {
    719         IFolder genFolder = mProject.getFolder(SdkConstants.FD_GEN_SOURCES);
    720         if (genFolder != null && genFolder.exists()) {
    721             ManifestInfo manifestInfo = ManifestInfo.get(mProject);
    722             String pkg = manifestInfo.getPackage();
    723             if (pkg != null) {
    724                 IFile rFile = genFolder.getFile(pkg.replace('.', '/') + '/' + FN_RESOURCE_CLASS);
    725                 TextChange change = getTextChange(rFile);
    726                 if (change != null) {
    727                     change.setEnabled(false);
    728                 }
    729             }
    730         }
    731     }
    732 
    733     /**
    734      * Searches for existing changes in the refactoring which modifies the R
    735      * field to rename it. it's derived so performing this change will generate
    736      * a "generated code was modified manually" warning
    737      *
    738      * @param change the change to disable R file changes in
    739      */
    740     public static void disableRClassChanges(Change change) {
    741         if (change.getName().equals(FN_RESOURCE_CLASS)) {
    742             change.setEnabled(false);
    743         }
    744         // Look for the field change on the R.java class; it's a derived file
    745         // and will generate file modified manually warnings. Disable it.
    746         if (change instanceof CompositeChange) {
    747             for (Change outer : ((CompositeChange) change).getChildren()) {
    748                 disableRClassChanges(outer);
    749             }
    750         }
    751     }
    752 }
    753