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