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      */
RenameResourceParticipant()162     public RenameResourceParticipant() {
163     }
164 
165     @Override
getName()166     public String getName() {
167         return "Android Rename Field Participant";
168     }
169 
170     @Override
initialize(Object element)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 */
createFieldRefactoring(IField field)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      */
createFieldRefactoring( @onNull IField field, @NonNull String newName, boolean updateReferences)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 
createXmlSearchPatterns()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
checkConditions(IProgressMonitor pm, CheckConditionsContext context)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
createChange(IProgressMonitor monitor)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      */
addResourceFileChanges( CompositeChange change, IProject project, IProgressMonitor monitor)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 
addJavaChanges(CompositeChange result, IProgressMonitor monitor)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 
addResourceXmlChanges( IFile file, CompositeChange changes, ResourceFolderType folderType)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 
addReplacements( @onNull List<TextEdit> edits, @NonNull Element element, @NonNull IStructuredDocument document, @Nullable ResourceFolderType folderType)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      */
getFirstNonBlankIndex(String s)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
getResourceField( @onNull IProject project, @NonNull ResourceType type, @NonNull String name)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      */
disableExistingResourceFileChange()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      */
disableRClassChanges(Change change)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