diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java new file mode 100644 index 000000000..406cebca4 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.refactorings.renamepackage; + +import static com.android.SdkConstants.FN_BUILD_CONFIG_BASE; +import static com.android.SdkConstants.FN_MANIFEST_BASE; +import static com.android.SdkConstants.FN_RESOURCE_BASE; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceVisitor; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.text.edits.MalformedTreeException; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.text.edits.TextEditGroup; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; +import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Wrapper class defining the stages of the refactoring process + */ +@SuppressWarnings("restriction") +class ApplicationPackageNameRefactoring extends Refactoring { + private final IProject mProject; + private final Name mOldPackageName; + private final Name mNewPackageName; + + List<String> MAIN_COMPONENT_TYPES_LIST = Arrays.asList(MAIN_COMPONENT_TYPES); + + ApplicationPackageNameRefactoring( + IProject project, + Name oldPackageName, + Name newPackageName) { + mProject = project; + mOldPackageName = oldPackageName; + mNewPackageName = newPackageName; + } + + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor pm) + throws CoreException, OperationCanceledException { + + // Accurate refactoring of the "shorthand" names in + // AndroidManifest.xml depends on not having compilation errors. + if (mProject.findMaxProblemSeverity( + IMarker.PROBLEM, + true, + IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) { + return + RefactoringStatus.createFatalErrorStatus("Fix the errors in your project, first."); + } + + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor pm) + throws OperationCanceledException { + + return new RefactoringStatus(); + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + + // Traverse all files in the project, building up a list of changes + JavaFileVisitor fileVisitor = new JavaFileVisitor(); + mProject.accept(fileVisitor); + return fileVisitor.getChange(); + } + + @Override + public String getName() { + return "AndroidPackageNameRefactoring"; //$NON-NLS-1$ + } + + public final static String[] MAIN_COMPONENT_TYPES = { + AndroidManifest.NODE_ACTIVITY, AndroidManifest.NODE_SERVICE, + AndroidManifest.NODE_RECEIVER, AndroidManifest.NODE_PROVIDER, + AndroidManifest.NODE_APPLICATION + }; + + + TextEdit updateJavaFileImports(CompilationUnit cu) { + + ImportVisitor importVisitor = new ImportVisitor(cu.getAST()); + cu.accept(importVisitor); + TextEdit rewrittenImports = importVisitor.getTextEdit(); + + // If the import of R was potentially implicit, insert an import statement + if (rewrittenImports != null && cu.getPackage().getName().getFullyQualifiedName() + .equals(mOldPackageName.getFullyQualifiedName())) { + + UsageVisitor usageVisitor = new UsageVisitor(); + cu.accept(usageVisitor); + + if (usageVisitor.seenAny()) { + ImportRewrite irw = ImportRewrite.create(cu, true); + if (usageVisitor.hasSeenR()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_RESOURCE_BASE); + } + if (usageVisitor.hasSeenBuildConfig()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_BUILD_CONFIG_BASE); + } + if (usageVisitor.hasSeenManifest()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_MANIFEST_BASE); + } + + try { + rewrittenImports.addChild( irw.rewriteImports(null) ); + } catch (MalformedTreeException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + } + } + + return rewrittenImports; + } + + // XML utility functions + private String stripQuotes(String text) { + int len = text.length(); + if (len >= 2 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') { + return text.substring(1, len - 1); + } else if (len >= 2 && text.charAt(0) == '\'' && text.charAt(len - 1) == '\'') { + return text.substring(1, len - 1); + } + return text; + } + + private String addQuotes(String text) { + return '"' + text + '"'; + } + + /* + * Make the appropriate package name changes to a resource file, + * e.g. .xml files in res/layout. This entails updating the namespace + * declarations for custom styleable attributes. The namespace prefix + * is user-defined and may be declared in any element where or parent + * element of where the prefix is used. + */ + TextFileChange editXmlResourceFile(IFile file) { + + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredDocument sdoc = null; + try { + sdoc = modelManager.createStructuredDocumentFor(file); + } catch (IOException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + + if (sdoc == null) { + return null; + } + + TextFileChange xmlChange = new TextFileChange("XML resource file edit", file); + xmlChange.setTextType(SdkConstants.EXT_XML); + + MultiTextEdit multiEdit = new MultiTextEdit(); + ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); + + final String oldAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, + mOldPackageName.getFullyQualifiedName()); + final String newAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES, + mNewPackageName.getFullyQualifiedName()); + + // Prepare the change set + for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { + + if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + continue; + } + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String lastAttrName = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + // Memorize the last attribute name seen + lastAttrName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + // Check this is the attribute and the original string + + if (lastAttrName != null && + lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { + + String lastAttrValue = region.getText(subRegion); + if (oldAppNamespaceString.equals(stripQuotes(lastAttrValue))) { + + // Found an occurrence. Create a change for it. + TextEdit edit = new ReplaceEdit( + region.getStartOffset() + subRegion.getStart(), + subRegion.getTextLength(), + addQuotes(newAppNamespaceString)); + TextEditGroup editGroup = new TextEditGroup( + "Replace package name in custom namespace prefix", edit); + + multiEdit.addChild(edit); + editGroups.add(editGroup); + } + } + } + } + } + + if (multiEdit.hasChildren()) { + xmlChange.setEdit(multiEdit); + for (TextEditGroup group : editGroups) { + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); + } + + return xmlChange; + } + return null; + } + + /* + * Replace all instances of the package name in AndroidManifest.xml. + * This includes expanding shorthand paths for each Component (Activity, + * Service, etc.) and of course updating the application package name. + * The namespace prefix might not be "android", so we resolve it + * dynamically. + */ + TextFileChange editAndroidManifest(IFile file) { + + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredDocument sdoc = null; + try { + sdoc = modelManager.createStructuredDocumentFor(file); + } catch (IOException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + + if (sdoc == null) { + return null; + } + + TextFileChange xmlChange = new TextFileChange("Make Manifest edits", file); + xmlChange.setTextType(SdkConstants.EXT_XML); + + MultiTextEdit multiEdit = new MultiTextEdit(); + ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); + + // The namespace prefix is guaranteed to be resolved before + // the first use of this attribute + String android_name_attribute = null; + + // Prepare the change set + for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) { + + // Only look at XML "top regions" + if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) { + continue; + } + + int nb = region.getNumberOfRegions(); + ITextRegionList list = region.getRegions(); + String lastTagName = null, lastAttrName = null; + + for (int i = 0; i < nb; i++) { + ITextRegion subRegion = list.get(i); + String type = subRegion.getType(); + + if (DOMRegionContext.XML_TAG_NAME.equals(type)) { + // Memorize the last tag name seen + lastTagName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) { + // Memorize the last attribute name seen + lastAttrName = region.getText(subRegion); + + } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) { + + String lastAttrValue = region.getText(subRegion); + if (lastAttrName != null && + lastAttrName.startsWith(SdkConstants.XMLNS_PREFIX)) { + + // Resolves the android namespace prefix for this file + if (SdkConstants.ANDROID_URI.equals(stripQuotes(lastAttrValue))) { + String android_namespace_prefix = lastAttrName + .substring(SdkConstants.XMLNS_PREFIX.length()); + android_name_attribute = android_namespace_prefix + ':' + + AndroidManifest.ATTRIBUTE_NAME; + } + } else if (AndroidManifest.NODE_MANIFEST.equals(lastTagName) + && AndroidManifest.ATTRIBUTE_PACKAGE.equals(lastAttrName)) { + + // Found an occurrence. Create a change for it. + TextEdit edit = new ReplaceEdit(region.getStartOffset() + + subRegion.getStart(), subRegion.getTextLength(), + addQuotes(mNewPackageName.getFullyQualifiedName())); + + multiEdit.addChild(edit); + editGroups.add(new TextEditGroup("Change Android package name", edit)); + + } else if (MAIN_COMPONENT_TYPES_LIST.contains(lastTagName) + && lastAttrName != null + && lastAttrName.equals(android_name_attribute)) { + + String package_path = stripQuotes(lastAttrValue); + String old_package_name_string = mOldPackageName.getFullyQualifiedName(); + + String absolute_path = AndroidManifest.combinePackageAndClassName( + old_package_name_string, package_path); + + TextEdit edit = new ReplaceEdit(region.getStartOffset() + + subRegion.getStart(), subRegion.getTextLength(), + addQuotes(absolute_path)); + + multiEdit.addChild(edit); + + editGroups.add(new TextEditGroup("Update component path", edit)); + } + } + } + } + + if (multiEdit.hasChildren()) { + xmlChange.setEdit(multiEdit); + for (TextEditGroup group : editGroups) { + xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group)); + } + + return xmlChange; + } + return null; + } + + + /* + * Iterates through all project files, taking distinct actions based on + * whether the file is: + * 1) a .java file (replaces or inserts the "import" statements) + * 2) a .xml layout file (updates namespace declarations) + * 3) the AndroidManifest.xml + */ + class JavaFileVisitor implements IResourceVisitor { + + final List<TextFileChange> mChanges = new ArrayList<TextFileChange>(); + + final ASTParser mParser = ASTParser.newParser(AST.JLS3); + + public CompositeChange getChange() { + + Collections.reverse(mChanges); + CompositeChange change = new CompositeChange("Refactoring Application package name", + mChanges.toArray(new Change[mChanges.size()])); + change.markAsSynthetic(); + return change; + } + + @Override + public boolean visit(IResource resource) throws CoreException { + if (resource instanceof IFile) { + IFile file = (IFile) resource; + if (SdkConstants.EXT_JAVA.equals(file.getFileExtension())) { + + ICompilationUnit icu = JavaCore.createCompilationUnitFrom(file); + + mParser.setSource(icu); + CompilationUnit cu = (CompilationUnit) mParser.createAST(null); + + TextEdit textEdit = updateJavaFileImports(cu); + if (textEdit != null && textEdit.hasChildren()) { + MultiTextEdit edit = new MultiTextEdit(); + edit.addChild(textEdit); + + TextFileChange text_file_change = new TextFileChange(file.getName(), file); + text_file_change.setTextType(SdkConstants.EXT_JAVA); + text_file_change.setEdit(edit); + mChanges.add(text_file_change); + } + + // XXX Partially taken from ExtractStringRefactoring.java + // Check this a Layout XML file and get the selection and + // its context. + } else if (SdkConstants.EXT_XML.equals(file.getFileExtension())) { + + if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) { + // Ensure that this is the root manifest, not some other copy + // (such as the one in bin/) + IPath path = file.getFullPath(); + if (path.segmentCount() == 2) { + TextFileChange manifest_change = editAndroidManifest(file); + mChanges.add(manifest_change); + } + } else { + + // Currently we only support Android resource XML files, + // so they must have a path similar to + // project/res/<type>[-<configuration>]/*.xml + // There is no support for sub folders, so the segment count must be 4. + // We don't need to check the type folder name because + // a/ we only accept an AndroidXmlEditor source and + // b/ aapt generates a compilation error for unknown folders. + IPath path = file.getFullPath(); + // check if we are inside the project/res/* folder. + if (path.segmentCount() == 4) { + if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) { + + + TextFileChange xmlChange = editXmlResourceFile(file); + if (xmlChange != null) { + mChanges.add(xmlChange); + } + } + } + } + } + + return false; + + } else if (resource instanceof IFolder) { + return !SdkConstants.FD_GEN_SOURCES.equals(resource.getName()); + } + + return true; + } + } + + private static class UsageVisitor extends ASTVisitor { + private boolean mSeenManifest; + private boolean mSeenR; + private boolean mSeenBuildConfig; + + @Override + public boolean visit(QualifiedName node) { + Name qualifier = node.getQualifier(); + if (qualifier.isSimpleName()) { + String name = qualifier.toString(); + if (name.equals(FN_RESOURCE_BASE)) { + mSeenR = true; + } else if (name.equals(FN_BUILD_CONFIG_BASE)) { + mSeenBuildConfig = true; + } else if (name.equals(FN_MANIFEST_BASE)) { + mSeenManifest = true; + } + } + return super.visit(node); + }; + + public boolean seenAny() { + return mSeenR || mSeenBuildConfig || mSeenManifest; + } + + public boolean hasSeenBuildConfig() { + return mSeenBuildConfig; + } + public boolean hasSeenManifest() { + return mSeenManifest; + } + public boolean hasSeenR() { + return mSeenR; + } + } + + private class ImportVisitor extends ASTVisitor { + + final AST mAst; + final ASTRewrite mRewriter; + + ImportVisitor(AST ast) { + mAst = ast; + mRewriter = ASTRewrite.create(ast); + } + + public TextEdit getTextEdit() { + try { + return this.mRewriter.rewriteAST(); + } catch (JavaModelException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (IllegalArgumentException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } + return null; + } + + @Override + public boolean visit(ImportDeclaration id) { + + Name importName = id.getName(); + if (importName.isQualifiedName()) { + QualifiedName qualifiedImportName = (QualifiedName) importName; + + String identifier = qualifiedImportName.getName().getIdentifier(); + if (identifier.equals(FN_RESOURCE_BASE)) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + } else if (identifier.equals(FN_BUILD_CONFIG_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + + } else if (identifier.equals(FN_MANIFEST_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + } + } + + return true; + } + } +} |