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