blob: 406cebca4387b25ab9be8ac8223770302b0e99d6 [file] [log] [blame]
/*
* 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;
}
}
}