Home | History | Annotate | Download | only in core
      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.core;
     18 
     19 import static com.android.SdkConstants.ANDROID_URI;
     20 import static com.android.SdkConstants.ATTR_CLASS;
     21 import static com.android.SdkConstants.ATTR_CONTEXT;
     22 import static com.android.SdkConstants.ATTR_NAME;
     23 import static com.android.SdkConstants.ATTR_PACKAGE;
     24 import static com.android.SdkConstants.DOT_XML;
     25 import static com.android.SdkConstants.EXT_XML;
     26 import static com.android.SdkConstants.TOOLS_URI;
     27 import static com.android.SdkConstants.VIEW_FRAGMENT;
     28 import static com.android.SdkConstants.VIEW_TAG;
     29 
     30 import com.android.SdkConstants;
     31 import com.android.annotations.NonNull;
     32 import com.android.ide.common.xml.ManifestData;
     33 import com.android.ide.eclipse.adt.AdtConstants;
     34 import com.android.ide.eclipse.adt.AdtPlugin;
     35 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     36 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
     37 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     38 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     39 import com.android.resources.ResourceFolderType;
     40 import com.android.utils.SdkUtils;
     41 
     42 import org.eclipse.core.resources.IFile;
     43 import org.eclipse.core.resources.IFolder;
     44 import org.eclipse.core.resources.IProject;
     45 import org.eclipse.core.resources.IResource;
     46 import org.eclipse.core.runtime.CoreException;
     47 import org.eclipse.core.runtime.IPath;
     48 import org.eclipse.core.runtime.IProgressMonitor;
     49 import org.eclipse.core.runtime.OperationCanceledException;
     50 import org.eclipse.jdt.core.IJavaElement;
     51 import org.eclipse.jdt.core.IJavaProject;
     52 import org.eclipse.jdt.core.IPackageFragment;
     53 import org.eclipse.jdt.core.JavaModelException;
     54 import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange;
     55 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
     56 import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
     57 import org.eclipse.jface.text.IRegion;
     58 import org.eclipse.jface.text.Region;
     59 import org.eclipse.ltk.core.refactoring.Change;
     60 import org.eclipse.ltk.core.refactoring.CompositeChange;
     61 import org.eclipse.ltk.core.refactoring.FileStatusContext;
     62 import org.eclipse.ltk.core.refactoring.NullChange;
     63 import org.eclipse.ltk.core.refactoring.RefactoringStatus;
     64 import org.eclipse.ltk.core.refactoring.RefactoringStatusContext;
     65 import org.eclipse.ltk.core.refactoring.TextFileChange;
     66 import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
     67 import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
     68 import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
     69 import org.eclipse.text.edits.MultiTextEdit;
     70 import org.eclipse.text.edits.ReplaceEdit;
     71 import org.eclipse.text.edits.TextEdit;
     72 import org.eclipse.wst.sse.core.StructuredModelManager;
     73 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     74 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     75 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     76 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
     77 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
     78 import org.w3c.dom.Attr;
     79 import org.w3c.dom.Document;
     80 import org.w3c.dom.Element;
     81 import org.w3c.dom.NamedNodeMap;
     82 import org.w3c.dom.Node;
     83 import org.w3c.dom.NodeList;
     84 
     85 import java.io.IOException;
     86 import java.util.ArrayList;
     87 import java.util.Collection;
     88 import java.util.List;
     89 
     90 /**
     91  * A participant to participate in refactorings that rename a package in an Android project.
     92  * The class updates android manifest and the layout file
     93  * The user can suppress refactoring by disabling the "Update references" checkbox
     94  * <p>
     95  * Rename participants are registered via the extension point <code>
     96  * org.eclipse.ltk.core.refactoring.renameParticipants</code>.
     97  * Extensions to this extension point must therefore extend
     98  * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
     99  * </p>
    100  */
    101 @SuppressWarnings("restriction")
    102 public class AndroidPackageRenameParticipant extends RenameParticipant {
    103 
    104     private IProject mProject;
    105     private IFile mManifestFile;
    106     private IPackageFragment mPackageFragment;
    107     private String mOldPackage;
    108     private String mNewPackage;
    109     private String mAppPackage;
    110     private boolean mRefactoringAppPackage;
    111 
    112     @Override
    113     public String getName() {
    114         return "Android Package Rename";
    115     }
    116 
    117     @Override
    118     public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
    119             throws OperationCanceledException {
    120         if (mAppPackage.equals(mOldPackage) && !mRefactoringAppPackage) {
    121             IRegion region = null;
    122             Document document = DomUtilities.getDocument(mManifestFile);
    123             if (document != null && document.getDocumentElement() != null) {
    124                 Attr attribute = document.getDocumentElement().getAttributeNode(ATTR_PACKAGE);
    125                 if (attribute instanceof IndexedRegion) {
    126                     IndexedRegion ir = (IndexedRegion) attribute;
    127                     int start = ir.getStartOffset();
    128                     region = new Region(start, ir.getEndOffset() - start);
    129                 }
    130             }
    131             if (region == null) {
    132                 region = new Region(0, 0);
    133             }
    134             // There's no line wrapping in the error dialog, so split up the message into
    135             // individually digestible pieces of information
    136             RefactoringStatusContext ctx = new FileStatusContext(mManifestFile, region);
    137             RefactoringStatus status = RefactoringStatus.createInfoStatus(
    138                     "You are refactoring the same package as your application's " +
    139                     "package (specified in the manifest).\n", ctx);
    140             status.addInfo(
    141                     "Note that this refactoring does NOT also update your " +
    142                     "application package.", ctx);
    143             status.addInfo("The application package defines your application's identity.", ctx);
    144             status.addInfo(
    145                     "If you change it, then it is considered to be a different application.", ctx);
    146             status.addInfo("(Users of the previous version cannot update to the new version.)",
    147                     ctx);
    148             status.addInfo(
    149                     "The application package, and the package containing the code, can differ.",
    150                     ctx);
    151             status.addInfo(
    152                     "To really change application package, " +
    153                     "choose \"Android Tools\" > \"Rename  Application Package.\" " +
    154                     "from the project context menu.", ctx);
    155             return status;
    156         }
    157 
    158         return new RefactoringStatus();
    159     }
    160 
    161     @Override
    162     protected boolean initialize(final Object element) {
    163         mRefactoringAppPackage = false;
    164         try {
    165             // Only propose this refactoring if the "Update References" checkbox is set.
    166             if (!getArguments().getUpdateReferences()) {
    167                 return false;
    168             }
    169 
    170             if (element instanceof IPackageFragment) {
    171                 mPackageFragment = (IPackageFragment) element;
    172                 if (!mPackageFragment.containsJavaResources()) {
    173                     return false;
    174                 }
    175                 IJavaProject javaProject = (IJavaProject) mPackageFragment
    176                         .getAncestor(IJavaElement.JAVA_PROJECT);
    177                 mProject = javaProject.getProject();
    178                 IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
    179                         + SdkConstants.FN_ANDROID_MANIFEST_XML);
    180 
    181                 if (manifestResource == null || !manifestResource.exists()
    182                         || !(manifestResource instanceof IFile)) {
    183                     RefactoringUtil.logInfo("Invalid or missing the "
    184                             + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the "
    185                             + mProject.getName() + " project.");
    186                     return false;
    187                 }
    188                 mManifestFile = (IFile) manifestResource;
    189                 String packageName = mPackageFragment.getElementName();
    190                 ManifestData manifestData;
    191                 manifestData = AndroidManifestHelper.parseForData(mManifestFile);
    192                 if (manifestData == null) {
    193                     return false;
    194                 }
    195                 mAppPackage = manifestData.getPackage();
    196                 mOldPackage = packageName;
    197                 mNewPackage = getArguments().getNewName();
    198                 if (mOldPackage == null || mNewPackage == null) {
    199                     return false;
    200                 }
    201 
    202                 if (RefactoringUtil.isRefactorAppPackage()
    203                         && mAppPackage != null
    204                         && mAppPackage.equals(packageName)) {
    205                     mRefactoringAppPackage = true;
    206                 }
    207 
    208                 return true;
    209             }
    210         } catch (JavaModelException ignore) {
    211         }
    212         return false;
    213     }
    214 
    215 
    216     @Override
    217     public Change createChange(IProgressMonitor pm) throws CoreException,
    218             OperationCanceledException {
    219         if (pm.isCanceled()) {
    220             return null;
    221         }
    222         if (!getArguments().getUpdateReferences()) {
    223             return null;
    224         }
    225 
    226         RefactoringProcessor p = getProcessor();
    227         if (p instanceof RenameCompilationUnitProcessor) {
    228             RenameTypeProcessor rtp =
    229                     ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
    230             if (rtp != null) {
    231                 String pattern = rtp.getFilePatterns();
    232                 boolean updQualf = rtp.getUpdateQualifiedNames();
    233                 if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
    234                     // Do not propose this refactoring if the
    235                     // "Update fully qualified names in non-Java files" option is
    236                     // checked and the file patterns mention XML. [c.f. SDK bug 21589]
    237                     return null;
    238                 }
    239             }
    240         }
    241 
    242         IPath pkgPath = mPackageFragment.getPath();
    243         IPath genPath = mProject.getFullPath().append(SdkConstants.FD_GEN_SOURCES);
    244         if (genPath.isPrefixOf(pkgPath)) {
    245             RefactoringUtil.logInfo(getName() + ": Cannot rename generated package.");
    246             return null;
    247         }
    248         CompositeChange result = new CompositeChange(getName());
    249         result.markAsSynthetic();
    250 
    251         addManifestFileChanges(result);
    252 
    253         // Update layout files; we don't just need to react to custom view
    254         // changes, we need to update fragment references and even tool:context activity
    255         // references
    256         addLayoutFileChanges(mProject, result);
    257 
    258         // Also update in dependent projects
    259         ProjectState projectState = Sdk.getProjectState(mProject);
    260         if (projectState != null) {
    261             Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
    262             for (ProjectState parentProject : parentProjects) {
    263                 IProject project = parentProject.getProject();
    264                 addLayoutFileChanges(project, result);
    265             }
    266         }
    267 
    268         if (mRefactoringAppPackage) {
    269             Change genChange = getGenPackageChange(pm);
    270             if (genChange != null) {
    271                 result.add(genChange);
    272             }
    273 
    274             return new NullChange("Update Imports") {
    275                 @Override
    276                 public Change perform(IProgressMonitor monitor) throws CoreException {
    277                     FixImportsJob job = new FixImportsJob("Fix Rename Package",
    278                             mManifestFile, mNewPackage);
    279                     job.schedule(500);
    280 
    281                     // Not undoable: just return null instead of an undo-change.
    282                     return null;
    283                 }
    284             };
    285         }
    286 
    287         return (result.getChildren().length == 0) ? null : result;
    288     }
    289 
    290     /**
    291      * Returns Android gen package text change
    292      *
    293      * @param pm the progress monitor
    294      *
    295      * @return Android gen package text change
    296      * @throws CoreException if an error happens
    297      * @throws OperationCanceledException if the operation is canceled
    298      */
    299     public Change getGenPackageChange(IProgressMonitor pm) throws CoreException,
    300             OperationCanceledException {
    301         if (mRefactoringAppPackage) {
    302             IPackageFragment genJavaPackageFragment = getGenPackageFragment();
    303             if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) {
    304                 return new RenamePackageChange(genJavaPackageFragment, mNewPackage, true);
    305             }
    306         }
    307         return null;
    308     }
    309 
    310     /**
    311      * Return the gen package fragment
    312      */
    313     private IPackageFragment getGenPackageFragment() throws JavaModelException {
    314         IJavaProject javaProject = (IJavaProject) mPackageFragment
    315                 .getAncestor(IJavaElement.JAVA_PROJECT);
    316         if (javaProject != null && javaProject.isOpen()) {
    317             IProject project = javaProject.getProject();
    318             IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES);
    319             if (genFolder.exists()) {
    320                 String javaPackagePath = mAppPackage.replace('.', '/');
    321                 IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath);
    322                 IPackageFragment genPackageFragment = javaProject
    323                         .findPackageFragment(genJavaPackagePath);
    324                 return genPackageFragment;
    325             }
    326         }
    327         return null;
    328     }
    329 
    330     /**
    331      * Returns the new class name
    332      *
    333      * @param fqcn the fully qualified class name in the renamed package
    334      * @return the new class name
    335      */
    336     private String getNewClassName(String fqcn) {
    337         assert isInRenamedPackage(fqcn) : fqcn;
    338         int lastDot = fqcn.lastIndexOf('.');
    339         if (lastDot < 0) {
    340             return mNewPackage;
    341         }
    342         String name = fqcn.substring(lastDot, fqcn.length());
    343         String newClassName = mNewPackage + name;
    344         return newClassName;
    345     }
    346 
    347     private void addManifestFileChanges(CompositeChange result) {
    348         addXmlFileChanges(mManifestFile, result, true);
    349     }
    350 
    351     private void addLayoutFileChanges(IProject project, CompositeChange result) {
    352         try {
    353             // Update references in XML resource files
    354             IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
    355 
    356             IResource[] folders = resFolder.members();
    357             for (IResource folder : folders) {
    358                 String folderName = folder.getName();
    359                 ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
    360                 if (folderType != ResourceFolderType.LAYOUT) {
    361                     continue;
    362                 }
    363                 if (!(folder instanceof IFolder)) {
    364                     continue;
    365                 }
    366                 IResource[] files = ((IFolder) folder).members();
    367                 for (int i = 0; i < files.length; i++) {
    368                     IResource member = files[i];
    369                     if ((member instanceof IFile) && member.exists()) {
    370                         IFile file = (IFile) member;
    371                         String fileName = member.getName();
    372 
    373                         if (SdkUtils.endsWith(fileName, DOT_XML)) {
    374                             addXmlFileChanges(file, result, false);
    375                         }
    376                     }
    377                 }
    378             }
    379         } catch (CoreException e) {
    380             RefactoringUtil.log(e);
    381         }
    382     }
    383 
    384     private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) {
    385         IModelManager modelManager = StructuredModelManager.getModelManager();
    386         IStructuredModel model = null;
    387         try {
    388             model = modelManager.getExistingModelForRead(file);
    389             if (model == null) {
    390                 model = modelManager.getModelForRead(file);
    391             }
    392             if (model != null) {
    393                 IStructuredDocument document = model.getStructuredDocument();
    394                 if (model instanceof IDOMModel) {
    395                     IDOMModel domModel = (IDOMModel) model;
    396                     Element root = domModel.getDocument().getDocumentElement();
    397                     if (root != null) {
    398                         List<TextEdit> edits = new ArrayList<TextEdit>();
    399                         if (isManifest) {
    400                             addManifestReplacements(edits, root, document);
    401                         } else {
    402                             addLayoutReplacements(edits, root, document);
    403                         }
    404                         if (!edits.isEmpty()) {
    405                             MultiTextEdit rootEdit = new MultiTextEdit();
    406                             rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
    407                             TextFileChange change = new TextFileChange(file.getName(), file);
    408                             change.setTextType(EXT_XML);
    409                             change.setEdit(rootEdit);
    410                             changes.add(change);
    411                         }
    412                     }
    413                 } else {
    414                     return false;
    415                 }
    416             }
    417 
    418             return true;
    419         } catch (IOException e) {
    420             AdtPlugin.log(e, null);
    421         } catch (CoreException e) {
    422             AdtPlugin.log(e, null);
    423         } finally {
    424             if (model != null) {
    425                 model.releaseFromRead();
    426             }
    427         }
    428 
    429         return false;
    430     }
    431 
    432     private boolean isInRenamedPackage(String fqcn) {
    433         return fqcn.startsWith(mOldPackage)
    434                 && fqcn.length() > mOldPackage.length()
    435                 && fqcn.indexOf('.', mOldPackage.length() + 1) == -1;
    436     }
    437 
    438     private void addLayoutReplacements(
    439             @NonNull List<TextEdit> edits,
    440             @NonNull Element element,
    441             @NonNull IStructuredDocument document) {
    442         String tag = element.getTagName();
    443         if (isInRenamedPackage(tag)) {
    444             int start = RefactoringUtil.getTagNameRangeStart(element, document);
    445             if (start != -1) {
    446                 int end = start + tag.length();
    447                 edits.add(new ReplaceEdit(start, end - start, getNewClassName(tag)));
    448             }
    449         } else {
    450             Attr classNode = null;
    451             if (tag.equals(VIEW_TAG)) {
    452                 classNode = element.getAttributeNode(ATTR_CLASS);
    453             } else if (tag.equals(VIEW_FRAGMENT)) {
    454                 classNode = element.getAttributeNode(ATTR_CLASS);
    455                 if (classNode == null) {
    456                     classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
    457                 }
    458             } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
    459                 classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
    460                 if (classNode != null && classNode.getValue().startsWith(".")) { //$NON-NLS-1$
    461                     classNode = null;
    462                 }
    463             }
    464             if (classNode != null) {
    465                 String fqcn = classNode.getValue();
    466                 if (isInRenamedPackage(fqcn)) {
    467                     int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
    468                     if (start != -1) {
    469                         int end = start + fqcn.length();
    470                         edits.add(new ReplaceEdit(start, end - start, getNewClassName(fqcn)));
    471                     }
    472                 }
    473             }
    474         }
    475 
    476         NodeList children = element.getChildNodes();
    477         for (int i = 0, n = children.getLength(); i < n; i++) {
    478             Node child = children.item(i);
    479             if (child.getNodeType() == Node.ELEMENT_NODE) {
    480                 addLayoutReplacements(edits, (Element) child, document);
    481             }
    482         }
    483     }
    484 
    485     private void addManifestReplacements(
    486             @NonNull List<TextEdit> edits,
    487             @NonNull Element element,
    488             @NonNull IStructuredDocument document) {
    489         if (mRefactoringAppPackage &&
    490                 element == element.getOwnerDocument().getDocumentElement()) {
    491             // Update the app package declaration
    492             Attr pkg = element.getAttributeNode(ATTR_PACKAGE);
    493             if (pkg != null && pkg.getValue().equals(mOldPackage)) {
    494                 int start = RefactoringUtil.getAttributeValueRangeStart(pkg, document);
    495                 if (start != -1) {
    496                     int end = start + mOldPackage.length();
    497                     edits.add(new ReplaceEdit(start, end - start, mNewPackage));
    498                 }
    499             }
    500         }
    501 
    502         NamedNodeMap attributes = element.getAttributes();
    503         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    504             Attr attr = (Attr) attributes.item(i);
    505             if (!RefactoringUtil.isManifestClassAttribute(attr)) {
    506                 continue;
    507             }
    508 
    509             String value = attr.getValue();
    510             if (isInRenamedPackage(value)) {
    511                 int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    512                 if (start != -1) {
    513                     int end = start + value.length();
    514                     edits.add(new ReplaceEdit(start, end - start, getNewClassName(value)));
    515                 }
    516             } else if (value.startsWith(".")) {
    517                 // If we're renaming the app package
    518                 String fqcn = mAppPackage + value;
    519                 if (isInRenamedPackage(fqcn)) {
    520                     int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
    521                     if (start != -1) {
    522                         int end = start + value.length();
    523                         String newClassName = getNewClassName(fqcn);
    524                         if (mRefactoringAppPackage) {
    525                             newClassName = newClassName.substring(mNewPackage.length());
    526                         } else if (newClassName.startsWith(mOldPackage)
    527                                 && newClassName.charAt(mOldPackage.length()) == '.') {
    528                             newClassName = newClassName.substring(mOldPackage.length());
    529                         }
    530 
    531                         if (!newClassName.equals(value)) {
    532                             edits.add(new ReplaceEdit(start, end - start, newClassName));
    533                         }
    534                     }
    535                 }
    536             }
    537         }
    538 
    539         NodeList children = element.getChildNodes();
    540         for (int i = 0, n = children.getLength(); i < n; i++) {
    541             Node child = children.item(i);
    542             if (child.getNodeType() == Node.ELEMENT_NODE) {
    543                 addManifestReplacements(edits, (Element) child, document);
    544             }
    545         }
    546     }
    547 }
    548