/*
* 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 com.android.SdkConstants;
import com.android.ide.common.xml.ManifestData;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutChange;
import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutChangeDescription;
import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidLayoutFileChanges;
import com.android.ide.eclipse.adt.internal.refactorings.changes.AndroidPackageRenameChange;
import com.android.xml.AndroidManifest;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
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.NullProgressMonitor;
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.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 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 AndroidRenameParticipant {
private IPackageFragment mPackageFragment;
private boolean mIsPackage;
private Set<AndroidLayoutFileChanges> mFileChanges = new HashSet<AndroidLayoutFileChanges>();
@Override
public Change createChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
if (pm.isCanceled()) {
return null;
}
if (!getArguments().getUpdateReferences())
return null;
IPath pkgPath = mPackageFragment.getPath();
IJavaProject javaProject = (IJavaProject) mPackageFragment
.getAncestor(IJavaElement.JAVA_PROJECT);
IProject project = javaProject.getProject();
IPath genPath = project.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();
if (mAndroidManifest.exists()) {
if (mAndroidElements.size() > 0 || mIsPackage) {
getManifestDocument();
Change change = new AndroidPackageRenameChange(mAndroidManifest, mManager,
mDocument, mAndroidElements, mOldName, mNewName, mIsPackage);
if (change != null) {
result.add(change);
}
}
if (mIsPackage) {
Change genChange = getGenPackageChange(pm);
if (genChange != null) {
result.add(genChange);
}
}
// add layoutChange
for (AndroidLayoutFileChanges fileChange : mFileChanges) {
IFile file = fileChange.getFile();
ITextFileBufferManager lManager = FileBuffers.getTextFileBufferManager();
lManager.connect(file.getFullPath(), LocationKind.NORMALIZE,
new NullProgressMonitor());
ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(),
LocationKind.NORMALIZE);
IDocument lDocument = buffer.getDocument();
Change layoutChange = new AndroidLayoutChange(file, lDocument, lManager,
fileChange.getChanges());
if (layoutChange != null) {
result.add(layoutChange);
}
}
}
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
* @throws OperationCanceledException
*/
public Change getGenPackageChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
if (mIsPackage) {
IPackageFragment genJavaPackageFragment = getGenPackageFragment();
if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) {
return new RenamePackageChange(genJavaPackageFragment, mNewName, 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;
}
@Override
public String getName() {
return "Android Package Rename";
}
@Override
protected boolean initialize(final Object element) {
mIsPackage = false;
try {
if (element instanceof IPackageFragment) {
mPackageFragment = (IPackageFragment) element;
if (!mPackageFragment.containsJavaResources())
return false;
IJavaProject javaProject = (IJavaProject) mPackageFragment
.getAncestor(IJavaElement.JAVA_PROJECT);
IProject project = javaProject.getProject();
IResource manifestResource = project.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 " + project.getName()
+ " project.");
return false;
}
mAndroidManifest = (IFile) manifestResource;
String packageName = mPackageFragment.getElementName();
ManifestData manifestData;
manifestData = AndroidManifestHelper.parseForData(mAndroidManifest);
if (manifestData == null) {
return false;
}
mAppPackage = manifestData.getPackage();
mOldName = packageName;
mNewName = getArguments().getNewName();
if (mOldName == null || mNewName == null) {
return false;
}
if (RefactoringUtil.isRefactorAppPackage()
&& mAppPackage != null
&& mAppPackage.equals(packageName)) {
mIsPackage = true;
}
mAndroidElements = addAndroidElements();
try {
final IType type = javaProject.findType(SdkConstants.CLASS_VIEW);
final HashSet<IType> elements = new HashSet<IType>();
if (type != null) {
ITypeHierarchy hierarchy = type.newTypeHierarchy(
new NullProgressMonitor());
IType[] allSubtypes = hierarchy.getAllSubtypes(type);
for (IType subType : allSubtypes) {
IResource resource = subType.getResource();
// TODO: Handle library project downstream dependencies!
if (resource != null && project.equals(resource.getProject())) {
if (subType.getPackageFragment().equals(mPackageFragment)) {
elements.add(subType);
}
}
}
}
List<String> views = new ArrayList<String>();
for (IType elem : elements) {
views.add(elem.getFullyQualifiedName());
}
if (views.size() > 0) {
String[] classNames = views.toArray(new String[0]);
addLayoutChanges(project, classNames);
}
} catch (CoreException e) {
RefactoringUtil.log(e);
}
return mIsPackage || mAndroidElements.size() > 0 || mFileChanges.size() > 0;
}
} catch (JavaModelException ignore) {
}
return false;
}
/**
* Adds layout changes for project
*
* @param project the Android project
* @param classNames the layout classes
*/
private void addLayoutChanges(IProject project, String[] classNames) {
try {
IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES);
IResource[] layoutMembers = resFolder.members();
for (int j = 0; j < layoutMembers.length; j++) {
IResource resource = layoutMembers[j];
if (resource instanceof IFolder
&& resource.exists()
&& resource.getName().startsWith(SdkConstants.FD_RES_LAYOUT)) {
IFolder layoutFolder = (IFolder) resource;
IResource[] members = layoutFolder.members();
for (int i = 0; i < members.length; i++) {
IResource member = members[i];
if ((member instanceof IFile)
&& member.exists()
&& member.getName().endsWith(".xml")) { //$NON-NLS-1$
IFile file = (IFile) member;
Set<AndroidLayoutChangeDescription> changes =
parse(file, classNames);
if (changes.size() > 0) {
AndroidLayoutFileChanges fileChange =
new AndroidLayoutFileChanges(file);
fileChange.getChanges().addAll(changes);
mFileChanges.add(fileChange);
}
}
}
}
}
} catch (CoreException e) {
RefactoringUtil.log(e);
}
}
/**
* Searches the layout file for classes
*
* @param file the Android layout file
* @param classNames the layout classes
*/
private Set<AndroidLayoutChangeDescription> parse(IFile file, String[] classNames) {
Set<AndroidLayoutChangeDescription> changes =
new HashSet<AndroidLayoutChangeDescription>();
ITextFileBufferManager lManager = null;
try {
lManager = FileBuffers.getTextFileBufferManager();
lManager.connect(file.getFullPath(),
LocationKind.NORMALIZE, new NullProgressMonitor());
ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(),
LocationKind.NORMALIZE);
IDocument lDocument = buffer.getDocument();
IStructuredModel model = null;
try {
model = StructuredModelManager.getModelManager().
getExistingModelForRead(lDocument);
if (model == null) {
if (lDocument instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) lDocument;
model = StructuredModelManager.getModelManager().getModelForRead(
structuredDocument);
}
}
if (model != null) {
IDOMModel xmlModel = (IDOMModel) model;
IDOMDocument xmlDoc = xmlModel.getDocument();
NodeList nodes = xmlDoc.getElementsByTagName(SdkConstants.VIEW);
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
Node attributeNode = attributes
.getNamedItem(SdkConstants.ATTR_CLASS);
if (attributeNode instanceof Attr) {
Attr attribute = (Attr) attributeNode;
String value = attribute.getValue();
if (value != null) {
for (int j = 0; j < classNames.length; j++) {
String className = classNames[j];
if (value.equals(className)) {
String newClassName = getNewClassName(className);
AndroidLayoutChangeDescription layoutChange =
new AndroidLayoutChangeDescription(
className, newClassName,
AndroidLayoutChangeDescription.VIEW_TYPE);
changes.add(layoutChange);
}
}
}
}
}
}
for (int j = 0; j < classNames.length; j++) {
String className = classNames[j];
nodes = xmlDoc.getElementsByTagName(className);
for (int i = 0; i < nodes.getLength(); i++) {
String newClassName = getNewClassName(className);
AndroidLayoutChangeDescription layoutChange =
new AndroidLayoutChangeDescription(
className, newClassName,
AndroidLayoutChangeDescription.STANDALONE_TYPE);
changes.add(layoutChange);
}
}
}
} finally {
if (model != null) {
model.releaseFromRead();
}
}
} catch (CoreException ignore) {
} finally {
if (lManager != null) {
try {
lManager.disconnect(file.getFullPath(), LocationKind.NORMALIZE,
new NullProgressMonitor());
} catch (CoreException ignore) {
}
}
}
return changes;
}
/**
* Returns the new class name
*
* @param className the class name
* @return the new class name
*/
private String getNewClassName(String className) {
int lastDot = className.lastIndexOf("."); //$NON-NLS-1$
if (lastDot < 0) {
return mNewName;
}
String name = className.substring(lastDot, className.length());
String newClassName = mNewName + name;
return newClassName;
}
/**
* Returns the elements (activity, receiver, service ...)
* which have to be renamed
*
* @return the android elements
*/
private Map<String, String> addAndroidElements() {
Map<String, String> androidElements = new HashMap<String, String>();
IDocument document;
try {
document = getManifestDocument();
} catch (CoreException e) {
RefactoringUtil.log(e);
if (mManager != null) {
try {
mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE,
new NullProgressMonitor());
} catch (CoreException e1) {
RefactoringUtil.log(e1);
}
}
document = null;
return androidElements;
}
IStructuredModel model = null;
try {
model = StructuredModelManager.getModelManager().getExistingModelForRead(document);
if (model == null) {
if (document instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) document;
model = StructuredModelManager.getModelManager().getModelForRead(
structuredDocument);
}
}
if (model != null) {
IDOMModel xmlModel = (IDOMModel) model;
IDOMDocument xmlDoc = xmlModel.getDocument();
add(xmlDoc, androidElements, AndroidManifest.NODE_ACTIVITY,
AndroidManifest.ATTRIBUTE_NAME);
add(xmlDoc, androidElements, AndroidManifest.NODE_APPLICATION,
AndroidManifest.ATTRIBUTE_NAME);
add(xmlDoc, androidElements, AndroidManifest.NODE_PROVIDER,
AndroidManifest.ATTRIBUTE_NAME);
add(xmlDoc, androidElements, AndroidManifest.NODE_RECEIVER,
AndroidManifest.ATTRIBUTE_NAME);
add(xmlDoc, androidElements, AndroidManifest.NODE_SERVICE,
AndroidManifest.ATTRIBUTE_NAME);
}
} finally {
if (model != null) {
model.releaseFromRead();
}
}
return androidElements;
}
/**
* Adds the element (activity, receiver, service ...) to the map
*
* @param xmlDoc the document
* @param androidElements the map
* @param element the element
*/
private void add(IDOMDocument xmlDoc, Map<String, String> androidElements, String element,
String argument) {
NodeList nodes = xmlDoc.getElementsByTagName(element);
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, argument);
if (attribute != null) {
String value = attribute.getValue();
if (value != null) {
String fullName = AndroidManifest.combinePackageAndClassName(mAppPackage,
value);
if (RefactoringUtil.isRefactorAppPackage()) {
if (fullName != null && fullName.startsWith(mAppPackage)) {
boolean startWithDot = (value.charAt(0) == '.');
boolean hasDot = (value.indexOf('.') != -1);
if (!startWithDot && hasDot) {
androidElements.put(element, value);
}
}
} else {
if (fullName != null) {
String currentPackage = mPackageFragment.getElementName();
if (fullName.lastIndexOf('.') == currentPackage.length()
&& fullName.startsWith(currentPackage)) {
androidElements.put(element, value);
}
}
}
}
}
}
}
}
}