Home | History | Annotate | Download | only in renamepackage
      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