/* * 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 com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidConstants; import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor; import com.android.sdklib.SdkConstants; import com.android.sdklib.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); private final static String ANDROID_NS_URI = SdkConstants.NS_RESOURCES; private final static String NAMESPACE_DECLARATION_PREFIX = XmlnsAttributeDescriptor.XMLNS_COLON; 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 (cu.getPackage().getName().getFullyQualifiedName() .equals(mOldPackageName.getFullyQualifiedName())) { ImportRewrite irw = ImportRewrite.create(cu, true); irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + AndroidConstants.FN_RESOURCE_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(AndroidConstants.EXT_XML); MultiTextEdit multiEdit = new MultiTextEdit(); ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>(); final String oldAppNamespaceString = String.format(AndroidConstants.NS_CUSTOM_RESOURCES, mOldPackageName.getFullyQualifiedName()); final String newAppNamespaceString = String.format(AndroidConstants.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(NAMESPACE_DECLARATION_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(AndroidConstants.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(NAMESPACE_DECLARATION_PREFIX)) { // Resolves the android namespace prefix for this file if (ANDROID_NS_URI.equals(stripQuotes(lastAttrValue))) { String android_namespace_prefix = lastAttrName .substring(NAMESPACE_DECLARATION_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()])); return change; } @SuppressWarnings("unused") public boolean visit(IResource resource) throws CoreException { if (resource instanceof IFile) { IFile file = (IFile) resource; if (AndroidConstants.EXT_JAVA.equals(file.getFileExtension())) { ICompilationUnit icu = JavaCore.createCompilationUnitFrom(file); mParser.setSource(icu); CompilationUnit cu = (CompilationUnit) mParser.createAST(null); TextEdit text_edit = updateJavaFileImports(cu); if (text_edit.hasChildren()) { MultiTextEdit edit = new MultiTextEdit(); edit.addChild(text_edit); TextFileChange text_file_change = new TextFileChange(file.getName(), file); text_file_change.setTextType(AndroidConstants.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 (AndroidConstants.EXT_XML.equals(file.getFileExtension())) { if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) { 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; } } 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; if (qualifiedImportName.getName().getIdentifier() .equals(AndroidConstants.FN_RESOURCE_BASE)) { mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, null); } } return true; } } }