blob: b821777a539485824ef7e5706241510f34d534c3 [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.core;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_CONTEXT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PACKAGE;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.EXT_XML;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_TAG;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.ide.common.xml.ManifestData;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.ResourceFolderType;
import com.android.utils.SdkUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
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.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor;
import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.FileStatusContext;
import org.eclipse.ltk.core.refactoring.NullChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusContext;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
import org.eclipse.ltk.core.refactoring.participants.RenameParticipant;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* A participant to participate in refactorings that rename a package in an Android project.
* The class updates android manifest and the layout file
* The user can suppress refactoring by disabling the "Update references" checkbox
* <p>
* Rename participants are registered via the extension point <code>
* org.eclipse.ltk.core.refactoring.renameParticipants</code>.
* Extensions to this extension point must therefore extend
* <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>.
* </p>
*/
@SuppressWarnings("restriction")
public class AndroidPackageRenameParticipant extends RenameParticipant {
private IProject mProject;
private IFile mManifestFile;
private IPackageFragment mPackageFragment;
private String mOldPackage;
private String mNewPackage;
private String mAppPackage;
private boolean mRefactoringAppPackage;
@Override
public String getName() {
return "Android Package Rename";
}
@Override
public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context)
throws OperationCanceledException {
if (mAppPackage.equals(mOldPackage) && !mRefactoringAppPackage) {
IRegion region = null;
Document document = DomUtilities.getDocument(mManifestFile);
if (document != null && document.getDocumentElement() != null) {
Attr attribute = document.getDocumentElement().getAttributeNode(ATTR_PACKAGE);
if (attribute instanceof IndexedRegion) {
IndexedRegion ir = (IndexedRegion) attribute;
int start = ir.getStartOffset();
region = new Region(start, ir.getEndOffset() - start);
}
}
if (region == null) {
region = new Region(0, 0);
}
// There's no line wrapping in the error dialog, so split up the message into
// individually digestible pieces of information
RefactoringStatusContext ctx = new FileStatusContext(mManifestFile, region);
RefactoringStatus status = RefactoringStatus.createInfoStatus(
"You are refactoring the same package as your application's " +
"package (specified in the manifest).\n", ctx);
status.addInfo(
"Note that this refactoring does NOT also update your " +
"application package.", ctx);
status.addInfo("The application package defines your application's identity.", ctx);
status.addInfo(
"If you change it, then it is considered to be a different application.", ctx);
status.addInfo("(Users of the previous version cannot update to the new version.)",
ctx);
status.addInfo(
"The application package, and the package containing the code, can differ.",
ctx);
status.addInfo(
"To really change application package, " +
"choose \"Android Tools\" > \"Rename Application Package.\" " +
"from the project context menu.", ctx);
return status;
}
return new RefactoringStatus();
}
@Override
protected boolean initialize(final Object element) {
mRefactoringAppPackage = false;
try {
// Only propose this refactoring if the "Update References" checkbox is set.
if (!getArguments().getUpdateReferences()) {
return false;
}
if (element instanceof IPackageFragment) {
mPackageFragment = (IPackageFragment) element;
if (!mPackageFragment.containsJavaResources()) {
return false;
}
IJavaProject javaProject = (IJavaProject) mPackageFragment
.getAncestor(IJavaElement.JAVA_PROJECT);
mProject = javaProject.getProject();
IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
if (manifestResource == null || !manifestResource.exists()
|| !(manifestResource instanceof IFile)) {
RefactoringUtil.logInfo("Invalid or missing the "
+ SdkConstants.FN_ANDROID_MANIFEST_XML + " in the "
+ mProject.getName() + " project.");
return false;
}
mManifestFile = (IFile) manifestResource;
String packageName = mPackageFragment.getElementName();
ManifestData manifestData;
manifestData = AndroidManifestHelper.parseForData(mManifestFile);
if (manifestData == null) {
return false;
}
mAppPackage = manifestData.getPackage();
mOldPackage = packageName;
mNewPackage = getArguments().getNewName();
if (mOldPackage == null || mNewPackage == null) {
return false;
}
if (RefactoringUtil.isRefactorAppPackage()
&& mAppPackage != null
&& mAppPackage.equals(packageName)) {
mRefactoringAppPackage = true;
}
return true;
}
} catch (JavaModelException ignore) {
}
return false;
}
@Override
public Change createChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
if (pm.isCanceled()) {
return null;
}
if (!getArguments().getUpdateReferences()) {
return null;
}
RefactoringProcessor p = getProcessor();
if (p instanceof RenameCompilationUnitProcessor) {
RenameTypeProcessor rtp =
((RenameCompilationUnitProcessor) p).getRenameTypeProcessor();
if (rtp != null) {
String pattern = rtp.getFilePatterns();
boolean updQualf = rtp.getUpdateQualifiedNames();
if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$
// Do not propose this refactoring if the
// "Update fully qualified names in non-Java files" option is
// checked and the file patterns mention XML. [c.f. SDK bug 21589]
return null;
}
}
}
IPath pkgPath = mPackageFragment.getPath();
IPath genPath = mProject.getFullPath().append(SdkConstants.FD_GEN_SOURCES);
if (genPath.isPrefixOf(pkgPath)) {
RefactoringUtil.logInfo(getName() + ": Cannot rename generated package.");
return null;
}
CompositeChange result = new CompositeChange(getName());
result.markAsSynthetic();
addManifestFileChanges(result);
// Update layout files; we don't just need to react to custom view
// changes, we need to update fragment references and even tool:context activity
// references
addLayoutFileChanges(mProject, result);
// Also update in dependent projects
ProjectState projectState = Sdk.getProjectState(mProject);
if (projectState != null) {
Collection<ProjectState> parentProjects = projectState.getFullParentProjects();
for (ProjectState parentProject : parentProjects) {
IProject project = parentProject.getProject();
addLayoutFileChanges(project, result);
}
}
if (mRefactoringAppPackage) {
Change genChange = getGenPackageChange(pm);
if (genChange != null) {
result.add(genChange);
}
return new NullChange("Update Imports") {
@Override
public Change perform(IProgressMonitor monitor) throws CoreException {
FixImportsJob job = new FixImportsJob("Fix Rename Package",
mManifestFile, mNewPackage);
job.schedule(500);
// Not undoable: just return null instead of an undo-change.
return null;
}
};
}
return (result.getChildren().length == 0) ? null : result;
}
/**
* Returns Android gen package text change
*
* @param pm the progress monitor
*
* @return Android gen package text change
* @throws CoreException if an error happens
* @throws OperationCanceledException if the operation is canceled
*/
public Change getGenPackageChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
if (mRefactoringAppPackage) {
IPackageFragment genJavaPackageFragment = getGenPackageFragment();
if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) {
return new RenamePackageChange(genJavaPackageFragment, mNewPackage, true);
}
}
return null;
}
/**
* Return the gen package fragment
*/
private IPackageFragment getGenPackageFragment() throws JavaModelException {
IJavaProject javaProject = (IJavaProject) mPackageFragment
.getAncestor(IJavaElement.JAVA_PROJECT);
if (javaProject != null && javaProject.isOpen()) {
IProject project = javaProject.getProject();
IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES);
if (genFolder.exists()) {
String javaPackagePath = mAppPackage.replace('.', '/');
IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath);
IPackageFragment genPackageFragment = javaProject
.findPackageFragment(genJavaPackagePath);
return genPackageFragment;
}
}
return null;
}
/**
* Returns the new class name
*
* @param fqcn the fully qualified class name in the renamed package
* @return the new class name
*/
private String getNewClassName(String fqcn) {
assert isInRenamedPackage(fqcn) : fqcn;
int lastDot = fqcn.lastIndexOf('.');
if (lastDot < 0) {
return mNewPackage;
}
String name = fqcn.substring(lastDot, fqcn.length());
String newClassName = mNewPackage + name;
return newClassName;
}
private void addManifestFileChanges(CompositeChange result) {
addXmlFileChanges(mManifestFile, result, true);
}
private void addLayoutFileChanges(IProject project, CompositeChange result) {
try {
// Update references in XML resource files
IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
IResource[] folders = resFolder.members();
for (IResource folder : folders) {
String folderName = folder.getName();
ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName);
if (folderType != ResourceFolderType.LAYOUT) {
continue;
}
if (!(folder instanceof IFolder)) {
continue;
}
IResource[] files = ((IFolder) folder).members();
for (int i = 0; i < files.length; i++) {
IResource member = files[i];
if ((member instanceof IFile) && member.exists()) {
IFile file = (IFile) member;
String fileName = member.getName();
if (SdkUtils.endsWith(fileName, DOT_XML)) {
addXmlFileChanges(file, result, false);
}
}
}
}
} catch (CoreException e) {
RefactoringUtil.log(e);
}
}
private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) {
IModelManager modelManager = StructuredModelManager.getModelManager();
IStructuredModel model = null;
try {
model = modelManager.getExistingModelForRead(file);
if (model == null) {
model = modelManager.getModelForRead(file);
}
if (model != null) {
IStructuredDocument document = model.getStructuredDocument();
if (model instanceof IDOMModel) {
IDOMModel domModel = (IDOMModel) model;
Element root = domModel.getDocument().getDocumentElement();
if (root != null) {
List<TextEdit> edits = new ArrayList<TextEdit>();
if (isManifest) {
addManifestReplacements(edits, root, document);
} else {
addLayoutReplacements(edits, root, document);
}
if (!edits.isEmpty()) {
MultiTextEdit rootEdit = new MultiTextEdit();
rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()]));
TextFileChange change = new TextFileChange(file.getName(), file);
change.setTextType(EXT_XML);
change.setEdit(rootEdit);
changes.add(change);
}
}
} else {
return false;
}
}
return true;
} catch (IOException e) {
AdtPlugin.log(e, null);
} catch (CoreException e) {
AdtPlugin.log(e, null);
} finally {
if (model != null) {
model.releaseFromRead();
}
}
return false;
}
private boolean isInRenamedPackage(String fqcn) {
return fqcn.startsWith(mOldPackage)
&& fqcn.length() > mOldPackage.length()
&& fqcn.indexOf('.', mOldPackage.length() + 1) == -1;
}
private void addLayoutReplacements(
@NonNull List<TextEdit> edits,
@NonNull Element element,
@NonNull IStructuredDocument document) {
String tag = element.getTagName();
if (isInRenamedPackage(tag)) {
int start = RefactoringUtil.getTagNameRangeStart(element, document);
if (start != -1) {
int end = start + tag.length();
edits.add(new ReplaceEdit(start, end - start, getNewClassName(tag)));
}
} else {
Attr classNode = null;
if (tag.equals(VIEW_TAG)) {
classNode = element.getAttributeNode(ATTR_CLASS);
} else if (tag.equals(VIEW_FRAGMENT)) {
classNode = element.getAttributeNode(ATTR_CLASS);
if (classNode == null) {
classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
}
} else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) {
classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT);
if (classNode != null && classNode.getValue().startsWith(".")) { //$NON-NLS-1$
classNode = null;
}
}
if (classNode != null) {
String fqcn = classNode.getValue();
if (isInRenamedPackage(fqcn)) {
int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document);
if (start != -1) {
int end = start + fqcn.length();
edits.add(new ReplaceEdit(start, end - start, getNewClassName(fqcn)));
}
}
}
}
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addLayoutReplacements(edits, (Element) child, document);
}
}
}
private void addManifestReplacements(
@NonNull List<TextEdit> edits,
@NonNull Element element,
@NonNull IStructuredDocument document) {
if (mRefactoringAppPackage &&
element == element.getOwnerDocument().getDocumentElement()) {
// Update the app package declaration
Attr pkg = element.getAttributeNode(ATTR_PACKAGE);
if (pkg != null && pkg.getValue().equals(mOldPackage)) {
int start = RefactoringUtil.getAttributeValueRangeStart(pkg, document);
if (start != -1) {
int end = start + mOldPackage.length();
edits.add(new ReplaceEdit(start, end - start, mNewPackage));
}
}
}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attr = (Attr) attributes.item(i);
if (!RefactoringUtil.isManifestClassAttribute(attr)) {
continue;
}
String value = attr.getValue();
if (isInRenamedPackage(value)) {
int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
if (start != -1) {
int end = start + value.length();
edits.add(new ReplaceEdit(start, end - start, getNewClassName(value)));
}
} else if (value.startsWith(".")) {
// If we're renaming the app package
String fqcn = mAppPackage + value;
if (isInRenamedPackage(fqcn)) {
int start = RefactoringUtil.getAttributeValueRangeStart(attr, document);
if (start != -1) {
int end = start + value.length();
String newClassName = getNewClassName(fqcn);
if (mRefactoringAppPackage) {
newClassName = newClassName.substring(mNewPackage.length());
} else if (newClassName.startsWith(mOldPackage)
&& newClassName.charAt(mOldPackage.length()) == '.') {
newClassName = newClassName.substring(mOldPackage.length());
}
if (!newClassName.equals(value)) {
edits.add(new ReplaceEdit(start, end - start, newClassName));
}
}
}
}
}
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addManifestReplacements(edits, (Element) child, document);
}
}
}
}