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