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.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