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