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_URI; 20 import static com.android.SdkConstants.ATTR_CLASS; 21 import static com.android.SdkConstants.ATTR_CONTEXT; 22 import static com.android.SdkConstants.ATTR_NAME; 23 import static com.android.SdkConstants.ATTR_PACKAGE; 24 import static com.android.SdkConstants.DOT_XML; 25 import static com.android.SdkConstants.EXT_XML; 26 import static com.android.SdkConstants.TOOLS_URI; 27 import static com.android.SdkConstants.VIEW_FRAGMENT; 28 import static com.android.SdkConstants.VIEW_TAG; 29 30 import com.android.SdkConstants; 31 import com.android.annotations.NonNull; 32 import com.android.ide.common.xml.ManifestData; 33 import com.android.ide.eclipse.adt.AdtConstants; 34 import com.android.ide.eclipse.adt.AdtPlugin; 35 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; 36 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 37 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 38 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 39 import com.android.resources.ResourceFolderType; 40 import com.android.utils.SdkUtils; 41 42 import org.eclipse.core.resources.IFile; 43 import org.eclipse.core.resources.IFolder; 44 import org.eclipse.core.resources.IProject; 45 import org.eclipse.core.resources.IResource; 46 import org.eclipse.core.runtime.CoreException; 47 import org.eclipse.core.runtime.IPath; 48 import org.eclipse.core.runtime.IProgressMonitor; 49 import org.eclipse.core.runtime.OperationCanceledException; 50 import org.eclipse.jdt.core.IJavaElement; 51 import org.eclipse.jdt.core.IJavaProject; 52 import org.eclipse.jdt.core.IPackageFragment; 53 import org.eclipse.jdt.core.JavaModelException; 54 import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange; 55 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor; 56 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor; 57 import org.eclipse.jface.text.IRegion; 58 import org.eclipse.jface.text.Region; 59 import org.eclipse.ltk.core.refactoring.Change; 60 import org.eclipse.ltk.core.refactoring.CompositeChange; 61 import org.eclipse.ltk.core.refactoring.FileStatusContext; 62 import org.eclipse.ltk.core.refactoring.NullChange; 63 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 64 import org.eclipse.ltk.core.refactoring.RefactoringStatusContext; 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.text.edits.MultiTextEdit; 70 import org.eclipse.text.edits.ReplaceEdit; 71 import org.eclipse.text.edits.TextEdit; 72 import org.eclipse.wst.sse.core.StructuredModelManager; 73 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 74 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 75 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 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.Document; 80 import org.w3c.dom.Element; 81 import org.w3c.dom.NamedNodeMap; 82 import org.w3c.dom.Node; 83 import org.w3c.dom.NodeList; 84 85 import java.io.IOException; 86 import java.util.ArrayList; 87 import java.util.Collection; 88 import java.util.List; 89 90 /** 91 * A participant to participate in refactorings that rename a package in an Android project. 92 * The class updates android manifest and the layout file 93 * The user can suppress refactoring by disabling the "Update references" checkbox 94 * <p> 95 * Rename participants are registered via the extension point <code> 96 * org.eclipse.ltk.core.refactoring.renameParticipants</code>. 97 * Extensions to this extension point must therefore extend 98 * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>. 99 * </p> 100 */ 101 @SuppressWarnings("restriction") 102 public class AndroidPackageRenameParticipant extends RenameParticipant { 103 104 private IProject mProject; 105 private IFile mManifestFile; 106 private IPackageFragment mPackageFragment; 107 private String mOldPackage; 108 private String mNewPackage; 109 private String mAppPackage; 110 private boolean mRefactoringAppPackage; 111 112 @Override 113 public String getName() { 114 return "Android Package Rename"; 115 } 116 117 @Override 118 public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) 119 throws OperationCanceledException { 120 if (mAppPackage.equals(mOldPackage) && !mRefactoringAppPackage) { 121 IRegion region = null; 122 Document document = DomUtilities.getDocument(mManifestFile); 123 if (document != null && document.getDocumentElement() != null) { 124 Attr attribute = document.getDocumentElement().getAttributeNode(ATTR_PACKAGE); 125 if (attribute instanceof IndexedRegion) { 126 IndexedRegion ir = (IndexedRegion) attribute; 127 int start = ir.getStartOffset(); 128 region = new Region(start, ir.getEndOffset() - start); 129 } 130 } 131 if (region == null) { 132 region = new Region(0, 0); 133 } 134 // There's no line wrapping in the error dialog, so split up the message into 135 // individually digestible pieces of information 136 RefactoringStatusContext ctx = new FileStatusContext(mManifestFile, region); 137 RefactoringStatus status = RefactoringStatus.createInfoStatus( 138 "You are refactoring the same package as your application's " + 139 "package (specified in the manifest).\n", ctx); 140 status.addInfo( 141 "Note that this refactoring does NOT also update your " + 142 "application package.", ctx); 143 status.addInfo("The application package defines your application's identity.", ctx); 144 status.addInfo( 145 "If you change it, then it is considered to be a different application.", ctx); 146 status.addInfo("(Users of the previous version cannot update to the new version.)", 147 ctx); 148 status.addInfo( 149 "The application package, and the package containing the code, can differ.", 150 ctx); 151 status.addInfo( 152 "To really change application package, " + 153 "choose \"Android Tools\" > \"Rename Application Package.\" " + 154 "from the project context menu.", ctx); 155 return status; 156 } 157 158 return new RefactoringStatus(); 159 } 160 161 @Override 162 protected boolean initialize(final Object element) { 163 mRefactoringAppPackage = false; 164 try { 165 // Only propose this refactoring if the "Update References" checkbox is set. 166 if (!getArguments().getUpdateReferences()) { 167 return false; 168 } 169 170 if (element instanceof IPackageFragment) { 171 mPackageFragment = (IPackageFragment) element; 172 if (!mPackageFragment.containsJavaResources()) { 173 return false; 174 } 175 IJavaProject javaProject = (IJavaProject) mPackageFragment 176 .getAncestor(IJavaElement.JAVA_PROJECT); 177 mProject = javaProject.getProject(); 178 IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP 179 + SdkConstants.FN_ANDROID_MANIFEST_XML); 180 181 if (manifestResource == null || !manifestResource.exists() 182 || !(manifestResource instanceof IFile)) { 183 RefactoringUtil.logInfo("Invalid or missing the " 184 + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " 185 + mProject.getName() + " project."); 186 return false; 187 } 188 mManifestFile = (IFile) manifestResource; 189 String packageName = mPackageFragment.getElementName(); 190 ManifestData manifestData; 191 manifestData = AndroidManifestHelper.parseForData(mManifestFile); 192 if (manifestData == null) { 193 return false; 194 } 195 mAppPackage = manifestData.getPackage(); 196 mOldPackage = packageName; 197 mNewPackage = getArguments().getNewName(); 198 if (mOldPackage == null || mNewPackage == null) { 199 return false; 200 } 201 202 if (RefactoringUtil.isRefactorAppPackage() 203 && mAppPackage != null 204 && mAppPackage.equals(packageName)) { 205 mRefactoringAppPackage = true; 206 } 207 208 return true; 209 } 210 } catch (JavaModelException ignore) { 211 } 212 return false; 213 } 214 215 216 @Override 217 public Change createChange(IProgressMonitor pm) throws CoreException, 218 OperationCanceledException { 219 if (pm.isCanceled()) { 220 return null; 221 } 222 if (!getArguments().getUpdateReferences()) { 223 return null; 224 } 225 226 RefactoringProcessor p = getProcessor(); 227 if (p instanceof RenameCompilationUnitProcessor) { 228 RenameTypeProcessor rtp = 229 ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor(); 230 if (rtp != null) { 231 String pattern = rtp.getFilePatterns(); 232 boolean updQualf = rtp.getUpdateQualifiedNames(); 233 if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$ 234 // Do not propose this refactoring if the 235 // "Update fully qualified names in non-Java files" option is 236 // checked and the file patterns mention XML. [c.f. SDK bug 21589] 237 return null; 238 } 239 } 240 } 241 242 IPath pkgPath = mPackageFragment.getPath(); 243 IPath genPath = mProject.getFullPath().append(SdkConstants.FD_GEN_SOURCES); 244 if (genPath.isPrefixOf(pkgPath)) { 245 RefactoringUtil.logInfo(getName() + ": Cannot rename generated package."); 246 return null; 247 } 248 CompositeChange result = new CompositeChange(getName()); 249 result.markAsSynthetic(); 250 251 addManifestFileChanges(result); 252 253 // Update layout files; we don't just need to react to custom view 254 // changes, we need to update fragment references and even tool:context activity 255 // references 256 addLayoutFileChanges(mProject, result); 257 258 // Also update in dependent projects 259 ProjectState projectState = Sdk.getProjectState(mProject); 260 if (projectState != null) { 261 Collection<ProjectState> parentProjects = projectState.getFullParentProjects(); 262 for (ProjectState parentProject : parentProjects) { 263 IProject project = parentProject.getProject(); 264 addLayoutFileChanges(project, result); 265 } 266 } 267 268 if (mRefactoringAppPackage) { 269 Change genChange = getGenPackageChange(pm); 270 if (genChange != null) { 271 result.add(genChange); 272 } 273 274 return new NullChange("Update Imports") { 275 @Override 276 public Change perform(IProgressMonitor monitor) throws CoreException { 277 FixImportsJob job = new FixImportsJob("Fix Rename Package", 278 mManifestFile, mNewPackage); 279 job.schedule(500); 280 281 // Not undoable: just return null instead of an undo-change. 282 return null; 283 } 284 }; 285 } 286 287 return (result.getChildren().length == 0) ? null : result; 288 } 289 290 /** 291 * Returns Android gen package text change 292 * 293 * @param pm the progress monitor 294 * 295 * @return Android gen package text change 296 * @throws CoreException if an error happens 297 * @throws OperationCanceledException if the operation is canceled 298 */ 299 public Change getGenPackageChange(IProgressMonitor pm) throws CoreException, 300 OperationCanceledException { 301 if (mRefactoringAppPackage) { 302 IPackageFragment genJavaPackageFragment = getGenPackageFragment(); 303 if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) { 304 return new RenamePackageChange(genJavaPackageFragment, mNewPackage, true); 305 } 306 } 307 return null; 308 } 309 310 /** 311 * Return the gen package fragment 312 */ 313 private IPackageFragment getGenPackageFragment() throws JavaModelException { 314 IJavaProject javaProject = (IJavaProject) mPackageFragment 315 .getAncestor(IJavaElement.JAVA_PROJECT); 316 if (javaProject != null && javaProject.isOpen()) { 317 IProject project = javaProject.getProject(); 318 IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); 319 if (genFolder.exists()) { 320 String javaPackagePath = mAppPackage.replace('.', '/'); 321 IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath); 322 IPackageFragment genPackageFragment = javaProject 323 .findPackageFragment(genJavaPackagePath); 324 return genPackageFragment; 325 } 326 } 327 return null; 328 } 329 330 /** 331 * Returns the new class name 332 * 333 * @param fqcn the fully qualified class name in the renamed package 334 * @return the new class name 335 */ 336 private String getNewClassName(String fqcn) { 337 assert isInRenamedPackage(fqcn) : fqcn; 338 int lastDot = fqcn.lastIndexOf('.'); 339 if (lastDot < 0) { 340 return mNewPackage; 341 } 342 String name = fqcn.substring(lastDot, fqcn.length()); 343 String newClassName = mNewPackage + name; 344 return newClassName; 345 } 346 347 private void addManifestFileChanges(CompositeChange result) { 348 addXmlFileChanges(mManifestFile, result, true); 349 } 350 351 private void addLayoutFileChanges(IProject project, CompositeChange result) { 352 try { 353 // Update references in XML resource files 354 IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); 355 356 IResource[] folders = resFolder.members(); 357 for (IResource folder : folders) { 358 String folderName = folder.getName(); 359 ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); 360 if (folderType != ResourceFolderType.LAYOUT) { 361 continue; 362 } 363 if (!(folder instanceof IFolder)) { 364 continue; 365 } 366 IResource[] files = ((IFolder) folder).members(); 367 for (int i = 0; i < files.length; i++) { 368 IResource member = files[i]; 369 if ((member instanceof IFile) && member.exists()) { 370 IFile file = (IFile) member; 371 String fileName = member.getName(); 372 373 if (SdkUtils.endsWith(fileName, DOT_XML)) { 374 addXmlFileChanges(file, result, false); 375 } 376 } 377 } 378 } 379 } catch (CoreException e) { 380 RefactoringUtil.log(e); 381 } 382 } 383 384 private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) { 385 IModelManager modelManager = StructuredModelManager.getModelManager(); 386 IStructuredModel model = null; 387 try { 388 model = modelManager.getExistingModelForRead(file); 389 if (model == null) { 390 model = modelManager.getModelForRead(file); 391 } 392 if (model != null) { 393 IStructuredDocument document = model.getStructuredDocument(); 394 if (model instanceof IDOMModel) { 395 IDOMModel domModel = (IDOMModel) model; 396 Element root = domModel.getDocument().getDocumentElement(); 397 if (root != null) { 398 List<TextEdit> edits = new ArrayList<TextEdit>(); 399 if (isManifest) { 400 addManifestReplacements(edits, root, document); 401 } else { 402 addLayoutReplacements(edits, root, document); 403 } 404 if (!edits.isEmpty()) { 405 MultiTextEdit rootEdit = new MultiTextEdit(); 406 rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); 407 TextFileChange change = new TextFileChange(file.getName(), file); 408 change.setTextType(EXT_XML); 409 change.setEdit(rootEdit); 410 changes.add(change); 411 } 412 } 413 } else { 414 return false; 415 } 416 } 417 418 return true; 419 } catch (IOException e) { 420 AdtPlugin.log(e, null); 421 } catch (CoreException e) { 422 AdtPlugin.log(e, null); 423 } finally { 424 if (model != null) { 425 model.releaseFromRead(); 426 } 427 } 428 429 return false; 430 } 431 432 private boolean isInRenamedPackage(String fqcn) { 433 return fqcn.startsWith(mOldPackage) 434 && fqcn.length() > mOldPackage.length() 435 && fqcn.indexOf('.', mOldPackage.length() + 1) == -1; 436 } 437 438 private void addLayoutReplacements( 439 @NonNull List<TextEdit> edits, 440 @NonNull Element element, 441 @NonNull IStructuredDocument document) { 442 String tag = element.getTagName(); 443 if (isInRenamedPackage(tag)) { 444 int start = RefactoringUtil.getTagNameRangeStart(element, document); 445 if (start != -1) { 446 int end = start + tag.length(); 447 edits.add(new ReplaceEdit(start, end - start, getNewClassName(tag))); 448 } 449 } else { 450 Attr classNode = null; 451 if (tag.equals(VIEW_TAG)) { 452 classNode = element.getAttributeNode(ATTR_CLASS); 453 } else if (tag.equals(VIEW_FRAGMENT)) { 454 classNode = element.getAttributeNode(ATTR_CLASS); 455 if (classNode == null) { 456 classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); 457 } 458 } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) { 459 classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT); 460 if (classNode != null && classNode.getValue().startsWith(".")) { //$NON-NLS-1$ 461 classNode = null; 462 } 463 } 464 if (classNode != null) { 465 String fqcn = classNode.getValue(); 466 if (isInRenamedPackage(fqcn)) { 467 int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); 468 if (start != -1) { 469 int end = start + fqcn.length(); 470 edits.add(new ReplaceEdit(start, end - start, getNewClassName(fqcn))); 471 } 472 } 473 } 474 } 475 476 NodeList children = element.getChildNodes(); 477 for (int i = 0, n = children.getLength(); i < n; i++) { 478 Node child = children.item(i); 479 if (child.getNodeType() == Node.ELEMENT_NODE) { 480 addLayoutReplacements(edits, (Element) child, document); 481 } 482 } 483 } 484 485 private void addManifestReplacements( 486 @NonNull List<TextEdit> edits, 487 @NonNull Element element, 488 @NonNull IStructuredDocument document) { 489 if (mRefactoringAppPackage && 490 element == element.getOwnerDocument().getDocumentElement()) { 491 // Update the app package declaration 492 Attr pkg = element.getAttributeNode(ATTR_PACKAGE); 493 if (pkg != null && pkg.getValue().equals(mOldPackage)) { 494 int start = RefactoringUtil.getAttributeValueRangeStart(pkg, document); 495 if (start != -1) { 496 int end = start + mOldPackage.length(); 497 edits.add(new ReplaceEdit(start, end - start, mNewPackage)); 498 } 499 } 500 } 501 502 NamedNodeMap attributes = element.getAttributes(); 503 for (int i = 0, n = attributes.getLength(); i < n; i++) { 504 Attr attr = (Attr) attributes.item(i); 505 if (!RefactoringUtil.isManifestClassAttribute(attr)) { 506 continue; 507 } 508 509 String value = attr.getValue(); 510 if (isInRenamedPackage(value)) { 511 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); 512 if (start != -1) { 513 int end = start + value.length(); 514 edits.add(new ReplaceEdit(start, end - start, getNewClassName(value))); 515 } 516 } else if (value.startsWith(".")) { 517 // If we're renaming the app package 518 String fqcn = mAppPackage + value; 519 if (isInRenamedPackage(fqcn)) { 520 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); 521 if (start != -1) { 522 int end = start + value.length(); 523 String newClassName = getNewClassName(fqcn); 524 if (mRefactoringAppPackage) { 525 newClassName = newClassName.substring(mNewPackage.length()); 526 } else if (newClassName.startsWith(mOldPackage) 527 && newClassName.charAt(mOldPackage.length()) == '.') { 528 newClassName = newClassName.substring(mOldPackage.length()); 529 } 530 531 if (!newClassName.equals(value)) { 532 edits.add(new ReplaceEdit(start, end - start, newClassName)); 533 } 534 } 535 } 536 } 537 } 538 539 NodeList children = element.getChildNodes(); 540 for (int i = 0, n = children.getLength(); i < n; i++) { 541 Node child = children.item(i); 542 if (child.getNodeType() == Node.ELEMENT_NODE) { 543 addManifestReplacements(edits, (Element) child, document); 544 } 545 } 546 } 547 } 548