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.renamepackage; 18 19 import static com.android.SdkConstants.FN_BUILD_CONFIG_BASE; 20 import static com.android.SdkConstants.FN_MANIFEST_BASE; 21 import static com.android.SdkConstants.FN_RESOURCE_BASE; 22 23 import com.android.SdkConstants; 24 import com.android.ide.eclipse.adt.AdtConstants; 25 import com.android.ide.eclipse.adt.AdtPlugin; 26 import com.android.xml.AndroidManifest; 27 28 import org.eclipse.core.resources.IFile; 29 import org.eclipse.core.resources.IFolder; 30 import org.eclipse.core.resources.IMarker; 31 import org.eclipse.core.resources.IProject; 32 import org.eclipse.core.resources.IResource; 33 import org.eclipse.core.resources.IResourceVisitor; 34 import org.eclipse.core.runtime.CoreException; 35 import org.eclipse.core.runtime.IPath; 36 import org.eclipse.core.runtime.IProgressMonitor; 37 import org.eclipse.core.runtime.OperationCanceledException; 38 import org.eclipse.core.runtime.Status; 39 import org.eclipse.jdt.core.ICompilationUnit; 40 import org.eclipse.jdt.core.JavaCore; 41 import org.eclipse.jdt.core.JavaModelException; 42 import org.eclipse.jdt.core.dom.AST; 43 import org.eclipse.jdt.core.dom.ASTParser; 44 import org.eclipse.jdt.core.dom.ASTVisitor; 45 import org.eclipse.jdt.core.dom.CompilationUnit; 46 import org.eclipse.jdt.core.dom.ImportDeclaration; 47 import org.eclipse.jdt.core.dom.Name; 48 import org.eclipse.jdt.core.dom.QualifiedName; 49 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; 50 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; 51 import org.eclipse.ltk.core.refactoring.Change; 52 import org.eclipse.ltk.core.refactoring.CompositeChange; 53 import org.eclipse.ltk.core.refactoring.Refactoring; 54 import org.eclipse.ltk.core.refactoring.RefactoringStatus; 55 import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; 56 import org.eclipse.ltk.core.refactoring.TextFileChange; 57 import org.eclipse.text.edits.MalformedTreeException; 58 import org.eclipse.text.edits.MultiTextEdit; 59 import org.eclipse.text.edits.ReplaceEdit; 60 import org.eclipse.text.edits.TextEdit; 61 import org.eclipse.text.edits.TextEditGroup; 62 import org.eclipse.wst.sse.core.StructuredModelManager; 63 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 64 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 65 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; 66 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; 67 import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; 68 import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; 69 70 import java.io.IOException; 71 import java.util.ArrayList; 72 import java.util.Arrays; 73 import java.util.Collections; 74 import java.util.List; 75 76 /** 77 * Wrapper class defining the stages of the refactoring process 78 */ 79 @SuppressWarnings("restriction") 80 class ApplicationPackageNameRefactoring extends Refactoring { 81 private final IProject mProject; 82 private final Name mOldPackageName; 83 private final Name mNewPackageName; 84 85 List<String> MAIN_COMPONENT_TYPES_LIST = Arrays.asList(MAIN_COMPONENT_TYPES); 86 87 ApplicationPackageNameRefactoring( 88 IProject project, 89 Name oldPackageName, 90 Name newPackageName) { 91 mProject = project; 92 mOldPackageName = oldPackageName; 93 mNewPackageName = newPackageName; 94 } 95 96 @Override 97 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) 98 throws CoreException, OperationCanceledException { 99 100 // Accurate refactoring of the "shorthand" names in 101 // AndroidManifest.xml depends on not having compilation errors. 102 if (mProject.findMaxProblemSeverity( 103 IMarker.PROBLEM, 104 true, 105 IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) { 106 return 107 RefactoringStatus.createFatalErrorStatus("Fix the errors in your project, first."); 108 } 109 110 return new RefactoringStatus(); 111 } 112 113 @Override 114 public RefactoringStatus checkFinalConditions(IProgressMonitor pm) 115 throws OperationCanceledException { 116 117 return new RefactoringStatus(); 118 } 119 120 @Override 121 public Change createChange(IProgressMonitor pm) throws CoreException, 122 OperationCanceledException { 123 124 // Traverse all files in the project, building up a list of changes 125 JavaFileVisitor fileVisitor = new JavaFileVisitor(); 126 mProject.accept(fileVisitor); 127 return fileVisitor.getChange(); 128 } 129 130 @Override 131 public String getName() { 132 return "AndroidPackageNameRefactoring"; //$NON-NLS-1$ 133 } 134 135 public final static String[] MAIN_COMPONENT_TYPES = { 136 AndroidManifest.NODE_ACTIVITY, AndroidManifest.NODE_SERVICE, 137 AndroidManifest.NODE_RECEIVER, AndroidManifest.NODE_PROVIDER, 138 AndroidManifest.NODE_APPLICATION 139 }; 140 141 142 TextEdit updateJavaFileImports(CompilationUnit cu) { 143 144 ImportVisitor importVisitor = new ImportVisitor(cu.getAST()); 145 cu.accept(importVisitor); 146 TextEdit rewrittenImports = importVisitor.getTextEdit(); 147 148 // If the import of R was potentially implicit, insert an import statement 149 if (rewrittenImports != null && cu.getPackage().getName().getFullyQualifiedName() 150 .equals(mOldPackageName.getFullyQualifiedName())) { 151 152 UsageVisitor usageVisitor = new UsageVisitor(); 153 cu.accept(usageVisitor); 154 155 if (usageVisitor.seenAny()) { 156 ImportRewrite irw = ImportRewrite.create(cu, true); 157 if (usageVisitor.hasSeenR()) { 158 irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' 159 + FN_RESOURCE_BASE); 160 } 161 if (usageVisitor.hasSeenBuildConfig()) { 162 irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' 163 + FN_BUILD_CONFIG_BASE); 164 } 165 if (usageVisitor.hasSeenManifest()) { 166 irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' 167 + FN_MANIFEST_BASE); 168 } 169 170 try { 171 rewrittenImports.addChild( irw.rewriteImports(null) ); 172 } catch (MalformedTreeException e) { 173 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 174 AdtPlugin.getDefault().getLog().log(s); 175 } catch (CoreException e) { 176 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 177 AdtPlugin.getDefault().getLog().log(s); 178 } 179 } 180 } 181 182 return rewrittenImports; 183 } 184 185 // XML utility functions 186 private String stripQuotes(String text) { 187 int len = text.length(); 188 if (len >= 2 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') { 189 return text.substring(1, len - 1); 190 } else if (len >= 2 && text.charAt(0) == '\'' && text.charAt(len - 1) == '\'') { 191 return text.substring(1, len - 1); 192 } 193 return text; 194 } 195 196 private String addQuotes(String text) { 197 return '"' + text + '"'; 198 } 199 200 /* 201 * Make the appropriate package name changes to a resource file, 202 * e.g. .xml files in res/layout. This entails updating the namespace 203 * declarations for custom styleable attributes. The namespace prefix 204 * is user-defined and may be declared in any element where or parent 205 * element of where the prefix is used. 206 */ 207 TextFileChange editXmlResourceFile(IFile file) { 208 209 IModelManager modelManager = StructuredModelManager.getModelManager(); 210 IStructuredDocument sdoc = null; 211 try { 212 sdoc = modelManager.createStructuredDocumentFor(file); 213 } catch (IOException e) { 214 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 215 AdtPlugin.getDefault().getLog().log(s); 216 } catch (CoreException e) { 217 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 218 AdtPlugin.getDefault().getLog().log(s); 219 } 220 221 if (sdoc == null) { 222 return null; 223 } 224 225 TextFileChange xmlChange = new TextFileChange("XML resource file edit", file); 226 xmlChange.setTextType(SdkConstants.EXT_XML); 227 228 MultiTextEdit multiEdit = new MultiTextEdit(); 229 ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); 230 231 final String oldAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, 232 mOldPackageName.getFullyQualifiedName()); 233 final String newAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, 234 mNewPackageName.getFullyQualifiedName()); 235 236 // Prepare the change set 237 for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { 238 239 if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { 240 continue; 241 } 242 243 int nb = region.getNumberOfRegions(); 244 ITextRegionList list = region.getRegions(); 245 String lastAttrName = null; 246 247 for (int i = 0; i < nb; i++) { 248 ITextRegion subRegion = list.get(i); 249 String type = subRegion.getType(); 250 251 if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { 252 // Memorize the last attribute name seen 253 lastAttrName = region.getText(subRegion); 254 255 } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { 256 // Check this is the attribute and the original string 257 258 if (lastAttrName != null && 259 lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { 260 261 String lastAttrValue = region.getText(subRegion); 262 if (oldAppNamespaceString.equals(stripQuotes(lastAttrValue))) { 263 264 // Found an occurrence. Create a change for it. 265 TextEdit edit = new ReplaceEdit( 266 region.getStartOffset() + subRegion.getStart(), 267 subRegion.getTextLength(), 268 addQuotes(newAppNamespaceString)); 269 TextEditGroup editGroup = new TextEditGroup( 270 "Replace package name in custom namespace prefix", edit); 271 272 multiEdit.addChild(edit); 273 editGroups.add(editGroup); 274 } 275 } 276 } 277 } 278 } 279 280 if (multiEdit.hasChildren()) { 281 xmlChange.setEdit(multiEdit); 282 for (TextEditGroup group : editGroups) { 283 xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); 284 } 285 286 return xmlChange; 287 } 288 return null; 289 } 290 291 /* 292 * Replace all instances of the package name in AndroidManifest.xml. 293 * This includes expanding shorthand paths for each Component (Activity, 294 * Service, etc.) and of course updating the application package name. 295 * The namespace prefix might not be "android", so we resolve it 296 * dynamically. 297 */ 298 TextFileChange editAndroidManifest(IFile file) { 299 300 IModelManager modelManager = StructuredModelManager.getModelManager(); 301 IStructuredDocument sdoc = null; 302 try { 303 sdoc = modelManager.createStructuredDocumentFor(file); 304 } catch (IOException e) { 305 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 306 AdtPlugin.getDefault().getLog().log(s); 307 } catch (CoreException e) { 308 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 309 AdtPlugin.getDefault().getLog().log(s); 310 } 311 312 if (sdoc == null) { 313 return null; 314 } 315 316 TextFileChange xmlChange = new TextFileChange("Make Manifest edits", file); 317 xmlChange.setTextType(SdkConstants.EXT_XML); 318 319 MultiTextEdit multiEdit = new MultiTextEdit(); 320 ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); 321 322 // The namespace prefix is guaranteed to be resolved before 323 // the first use of this attribute 324 String android_name_attribute = null; 325 326 // Prepare the change set 327 for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { 328 329 // Only look at XML "top regions" 330 if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { 331 continue; 332 } 333 334 int nb = region.getNumberOfRegions(); 335 ITextRegionList list = region.getRegions(); 336 String lastTagName = null, lastAttrName = null; 337 338 for (int i = 0; i < nb; i++) { 339 ITextRegion subRegion = list.get(i); 340 String type = subRegion.getType(); 341 342 if (DOMRegionContext.XML_TAG_NAME.equals(type)) { 343 // Memorize the last tag name seen 344 lastTagName = region.getText(subRegion); 345 346 } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { 347 // Memorize the last attribute name seen 348 lastAttrName = region.getText(subRegion); 349 350 } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { 351 352 String lastAttrValue = region.getText(subRegion); 353 if (lastAttrName != null && 354 lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { 355 356 // Resolves the android namespace prefix for this file 357 if (SdkConstants.ANDROID_URI.equals(stripQuotes(lastAttrValue))) { 358 String android_namespace_prefix = lastAttrName 359 .substring(SdkConstants.XMLNS_PREFIX.length()); 360 android_name_attribute = android_namespace_prefix + ':' 361 + AndroidManifest.ATTRIBUTE_NAME; 362 } 363 } else if (AndroidManifest.NODE_MANIFEST.equals(lastTagName) 364 && AndroidManifest.ATTRIBUTE_PACKAGE.equals(lastAttrName)) { 365 366 // Found an occurrence. Create a change for it. 367 TextEdit edit = new ReplaceEdit(region.getStartOffset() 368 + subRegion.getStart(), subRegion.getTextLength(), 369 addQuotes(mNewPackageName.getFullyQualifiedName())); 370 371 multiEdit.addChild(edit); 372 editGroups.add(new TextEditGroup("Change Android package name", edit)); 373 374 } else if (MAIN_COMPONENT_TYPES_LIST.contains(lastTagName) 375 && lastAttrName != null 376 && lastAttrName.equals(android_name_attribute)) { 377 378 String package_path = stripQuotes(lastAttrValue); 379 String old_package_name_string = mOldPackageName.getFullyQualifiedName(); 380 381 String absolute_path = AndroidManifest.combinePackageAndClassName( 382 old_package_name_string, package_path); 383 384 TextEdit edit = new ReplaceEdit(region.getStartOffset() 385 + subRegion.getStart(), subRegion.getTextLength(), 386 addQuotes(absolute_path)); 387 388 multiEdit.addChild(edit); 389 390 editGroups.add(new TextEditGroup("Update component path", edit)); 391 } 392 } 393 } 394 } 395 396 if (multiEdit.hasChildren()) { 397 xmlChange.setEdit(multiEdit); 398 for (TextEditGroup group : editGroups) { 399 xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); 400 } 401 402 return xmlChange; 403 } 404 return null; 405 } 406 407 408 /* 409 * Iterates through all project files, taking distinct actions based on 410 * whether the file is: 411 * 1) a .java file (replaces or inserts the "import" statements) 412 * 2) a .xml layout file (updates namespace declarations) 413 * 3) the AndroidManifest.xml 414 */ 415 class JavaFileVisitor implements IResourceVisitor { 416 417 final List<TextFileChange> mChanges = new ArrayList<TextFileChange>(); 418 419 final ASTParser mParser = ASTParser.newParser(AST.JLS3); 420 421 public CompositeChange getChange() { 422 423 Collections.reverse(mChanges); 424 CompositeChange change = new CompositeChange("Refactoring Application package name", 425 mChanges.toArray(new Change[mChanges.size()])); 426 change.markAsSynthetic(); 427 return change; 428 } 429 430 @Override 431 public boolean visit(IResource resource) throws CoreException { 432 if (resource instanceof IFile) { 433 IFile file = (IFile) resource; 434 if (SdkConstants.EXT_JAVA.equals(file.getFileExtension())) { 435 436 ICompilationUnit icu = JavaCore.createCompilationUnitFrom(file); 437 438 mParser.setSource(icu); 439 CompilationUnit cu = (CompilationUnit) mParser.createAST(null); 440 441 TextEdit textEdit = updateJavaFileImports(cu); 442 if (textEdit != null && textEdit.hasChildren()) { 443 MultiTextEdit edit = new MultiTextEdit(); 444 edit.addChild(textEdit); 445 446 TextFileChange text_file_change = new TextFileChange(file.getName(), file); 447 text_file_change.setTextType(SdkConstants.EXT_JAVA); 448 text_file_change.setEdit(edit); 449 mChanges.add(text_file_change); 450 } 451 452 // XXX Partially taken from ExtractStringRefactoring.java 453 // Check this a Layout XML file and get the selection and 454 // its context. 455 } else if (SdkConstants.EXT_XML.equals(file.getFileExtension())) { 456 457 if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) { 458 // Ensure that this is the root manifest, not some other copy 459 // (such as the one in bin/) 460 IPath path = file.getFullPath(); 461 if (path.segmentCount() == 2) { 462 TextFileChange manifest_change = editAndroidManifest(file); 463 mChanges.add(manifest_change); 464 } 465 } else { 466 467 // Currently we only support Android resource XML files, 468 // so they must have a path similar to 469 // project/res/<type>[-<configuration>]/*.xml 470 // There is no support for sub folders, so the segment count must be 4. 471 // We don't need to check the type folder name because 472 // a/ we only accept an AndroidXmlEditor source and 473 // b/ aapt generates a compilation error for unknown folders. 474 IPath path = file.getFullPath(); 475 // check if we are inside the project/res/* folder. 476 if (path.segmentCount() == 4) { 477 if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { 478 479 480 TextFileChange xmlChange = editXmlResourceFile(file); 481 if (xmlChange != null) { 482 mChanges.add(xmlChange); 483 } 484 } 485 } 486 } 487 } 488 489 return false; 490 491 } else if (resource instanceof IFolder) { 492 return !SdkConstants.FD_GEN_SOURCES.equals(resource.getName()); 493 } 494 495 return true; 496 } 497 } 498 499 private static class UsageVisitor extends ASTVisitor { 500 private boolean mSeenManifest; 501 private boolean mSeenR; 502 private boolean mSeenBuildConfig; 503 504 @Override 505 public boolean visit(QualifiedName node) { 506 Name qualifier = node.getQualifier(); 507 if (qualifier.isSimpleName()) { 508 String name = qualifier.toString(); 509 if (name.equals(FN_RESOURCE_BASE)) { 510 mSeenR = true; 511 } else if (name.equals(FN_BUILD_CONFIG_BASE)) { 512 mSeenBuildConfig = true; 513 } else if (name.equals(FN_MANIFEST_BASE)) { 514 mSeenManifest = true; 515 } 516 } 517 return super.visit(node); 518 }; 519 520 public boolean seenAny() { 521 return mSeenR || mSeenBuildConfig || mSeenManifest; 522 } 523 524 public boolean hasSeenBuildConfig() { 525 return mSeenBuildConfig; 526 } 527 public boolean hasSeenManifest() { 528 return mSeenManifest; 529 } 530 public boolean hasSeenR() { 531 return mSeenR; 532 } 533 } 534 535 private class ImportVisitor extends ASTVisitor { 536 537 final AST mAst; 538 final ASTRewrite mRewriter; 539 540 ImportVisitor(AST ast) { 541 mAst = ast; 542 mRewriter = ASTRewrite.create(ast); 543 } 544 545 public TextEdit getTextEdit() { 546 try { 547 return this.mRewriter.rewriteAST(); 548 } catch (JavaModelException e) { 549 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 550 AdtPlugin.getDefault().getLog().log(s); 551 } catch (IllegalArgumentException e) { 552 Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); 553 AdtPlugin.getDefault().getLog().log(s); 554 } 555 return null; 556 } 557 558 @Override 559 public boolean visit(ImportDeclaration id) { 560 561 Name importName = id.getName(); 562 if (importName.isQualifiedName()) { 563 QualifiedName qualifiedImportName = (QualifiedName) importName; 564 565 String identifier = qualifiedImportName.getName().getIdentifier(); 566 if (identifier.equals(FN_RESOURCE_BASE)) { 567 mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, 568 null); 569 } else if (identifier.equals(FN_BUILD_CONFIG_BASE) 570 && mOldPackageName.toString().equals( 571 qualifiedImportName.getQualifier().toString())) { 572 mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, 573 null); 574 575 } else if (identifier.equals(FN_MANIFEST_BASE) 576 && mOldPackageName.toString().equals( 577 qualifiedImportName.getQualifier().toString())) { 578 mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, 579 null); 580 } 581 } 582 583 return true; 584 } 585 } 586 } 587