/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.jetbrains.android;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.intellij.history.LocalHistory;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.undo.DocumentReference;
import com.intellij.openapi.command.undo.DocumentReferenceManager;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlTag;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import com.intellij.refactoring.rename.RenameJavaVariableProcessor;
import com.intellij.refactoring.rename.RenamePsiElementProcessor;
import com.intellij.refactoring.rename.RenameXmlAttributeProcessor;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import org.jetbrains.android.dom.AndroidDomUtil;
import org.jetbrains.android.dom.resources.ResourceElement;
import org.jetbrains.android.dom.wrappers.LazyValueResourceElementWrapper;
import org.jetbrains.android.dom.wrappers.ValueResourceElementWrapper;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.resourceManagers.LocalResourceManager;
import org.jetbrains.android.resourceManagers.ResourceManager;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.*;
import static com.android.resources.ResourceType.DECLARE_STYLEABLE;
import static com.android.resources.ResourceType.STYLEABLE;
import static org.jetbrains.android.util.AndroidBundle.message;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidResourceRenameResourceProcessor extends RenamePsiElementProcessor {
// for tests
public static volatile boolean ASK = true;
@Override
public boolean canProcessElement(@NotNull final PsiElement element) {
return ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
final PsiElement element1 = LazyValueResourceElementWrapper.computeLazyElement(element);
if (element1 == null) {
return false;
}
if (element1 instanceof PsiFile) {
return AndroidFacet.getInstance(element1) != null && AndroidResourceUtil.isInResourceSubdirectory((PsiFile)element1, null);
}
else if (element1 instanceof PsiField) {
PsiField field = (PsiField)element1;
if (AndroidResourceUtil.isResourceField(field)) {
return AndroidResourceUtil.findResourcesByField(field).size() > 0;
}
}
else if (element1 instanceof XmlAttributeValue) {
LocalResourceManager manager = LocalResourceManager.getInstance(element1);
if (manager != null) {
if (AndroidResourceUtil.isIdDeclaration((XmlAttributeValue)element1)) {
return true;
}
// then it is value resource
XmlTag tag = PsiTreeUtil.getParentOfType(element1, XmlTag.class);
return tag != null &&
DomManager.getDomManager(tag.getProject()).getDomElement(tag) instanceof ResourceElement &&
manager.getValueResourceType(tag) != null;
}
}
else if (element1 instanceof PsiClass) {
PsiClass cls = (PsiClass)element1;
if (AndroidDomUtil.isInheritor(cls, CLASS_VIEW)) {
return true;
}
}
return false;
}
});
}
@Override
public void prepareRenaming(PsiElement element, String newName, Map<PsiElement, String> allRenames) {
final PsiElement element1 = LazyValueResourceElementWrapper.computeLazyElement(element);
if (element1 == null) {
return;
}
// TODO: support renaming alternative value resources
AndroidFacet facet = AndroidFacet.getInstance(element1);
assert facet != null;
if (element1 instanceof PsiFile) {
prepareResourceFileRenaming((PsiFile)element1, newName, allRenames, facet);
}
else if (element1 instanceof PsiClass) {
PsiClass cls = (PsiClass)element1;
if (AndroidDomUtil.isInheritor(cls, CLASS_VIEW)) {
prepareCustomViewRenaming(cls, newName, allRenames, facet);
}
}
else if (element1 instanceof XmlAttributeValue) {
XmlAttributeValue value = (XmlAttributeValue)element1;
if (AndroidResourceUtil.isIdDeclaration(value)) {
prepareIdRenaming(value, newName, allRenames, facet);
}
else {
prepareValueResourceRenaming(element1, newName, allRenames, facet);
}
}
else if (element1 instanceof PsiField) {
prepareResourceFieldRenaming((PsiField)element1, newName, allRenames);
}
}
private static void prepareCustomViewRenaming(PsiClass cls, String newName, Map<PsiElement, String> allRenames, AndroidFacet facet) {
AppResourceRepository appResources = AppResourceRepository.getAppResources(facet, true);
String oldName = cls.getName();
if (appResources.hasResourceItem(DECLARE_STYLEABLE, oldName)) {
LocalResourceManager manager = facet.getLocalResourceManager();
for (PsiElement element : manager.findResourcesByFieldName(STYLEABLE.getName(), oldName)) {
if (element instanceof XmlAttributeValue) {
if (element.getParent() instanceof XmlAttribute) {
XmlTag tag = ((XmlAttribute)element.getParent()).getParent();
String tagName = tag.getName();
if (tagName.equals(TAG_DECLARE_STYLEABLE)) {
// Rename main styleable field
for (PsiField field : AndroidResourceUtil.findResourceFields(facet, STYLEABLE.getName(), oldName, false)) {
String escaped = AndroidResourceUtil.getFieldNameByResourceName(newName);
allRenames.put(field, escaped);
}
// Rename dependent attribute fields
PsiField[] styleableFields = AndroidResourceUtil.findStyleableAttributeFields(tag, false);
if (styleableFields.length > 0) {
for (PsiField resField : styleableFields) {
String fieldName = resField.getName();
String newAttributeName;
if (fieldName.startsWith(oldName)) {
newAttributeName = newName + fieldName.substring(oldName.length());
}
else {
newAttributeName = oldName;
}
String escaped = AndroidResourceUtil.getFieldNameByResourceName(newAttributeName);
allRenames.put(resField, escaped);
}
}
}
}
}
}
}
}
private static void prepareIdRenaming(XmlAttributeValue value, String newName, Map<PsiElement, String> allRenames, AndroidFacet facet) {
LocalResourceManager manager = facet.getLocalResourceManager();
allRenames.remove(value);
String id = AndroidResourceUtil.getResourceNameByReferenceText(value.getValue());
assert id != null;
List<XmlAttributeValue> idDeclarations = manager.findIdDeclarations(id);
for (XmlAttributeValue idDeclaration : idDeclarations) {
// Only include explicit definitions (android:id). References through
// these are found via the normal rename refactoring usage search in
// RenamePsiElementProcessor#findReferences.
//
// And unfortunately, if we include declaration+references like
// android:labelFor="@+id/foo", we hit an assertion from the refactoring
// framework which looks related to elements getting modified multiple times.
if (!ATTR_ID.equals(((XmlAttribute)idDeclaration.getParent()).getLocalName())) {
continue;
}
allRenames.put(new ValueResourceElementWrapper(idDeclaration), newName);
}
String name = AndroidResourceUtil.getResourceNameByReferenceText(newName);
if (name != null) {
for (PsiField resField : AndroidResourceUtil.findIdFields(value)) {
allRenames.put(resField, AndroidResourceUtil.getFieldNameByResourceName(name));
}
}
}
@Nullable
private static String getResourceName(Project project, String newFieldName, String oldResourceName) {
if (newFieldName.indexOf('_') < 0) return newFieldName;
if (oldResourceName.indexOf('_') < 0 && oldResourceName.indexOf('.') >= 0) {
String suggestion = newFieldName.replace('_', '.');
newFieldName = Messages.showInputDialog(project, AndroidBundle.message("rename.resource.dialog.text", oldResourceName),
RefactoringBundle.message("rename.title"), Messages.getQuestionIcon(), suggestion, null);
}
return newFieldName;
}
private static void prepareResourceFieldRenaming(PsiField field, String newName, Map<PsiElement, String> allRenames) {
new RenameJavaVariableProcessor().prepareRenaming(field, newName, allRenames);
List<PsiElement> resources = AndroidResourceUtil.findResourcesByField(field);
PsiElement res = resources.get(0);
String resName = res instanceof XmlAttributeValue ? ((XmlAttributeValue)res).getValue() : ((PsiFile)res).getName();
final String newResName = getResourceName(field.getProject(), newName, resName);
for (PsiElement resource : resources) {
if (resource instanceof PsiFile) {
PsiFile file = (PsiFile)resource;
String extension = FileUtilRt.getExtension(file.getName());
allRenames.put(resource, newResName + '.' + extension);
}
else if (resource instanceof XmlAttributeValue) {
XmlAttributeValue value = (XmlAttributeValue)resource;
final String s = AndroidResourceUtil.isIdDeclaration(value)
? NEW_ID_PREFIX + newResName
: newResName;
allRenames.put(new ValueResourceElementWrapper(value), s);
// Also rename the dependent fields, e.g. if you rename <declare-styleable name="Foo">,
// we have to rename not just R.styleable.Foo but the also R.styleable.Foo_* attributes
if (value.getParent() instanceof XmlAttribute) {
XmlAttribute parent = (XmlAttribute)value.getParent();
XmlTag tag = parent.getParent();
if (tag.getName().equals(TAG_DECLARE_STYLEABLE)) {
AndroidFacet facet = AndroidFacet.getInstance(tag);
String oldName = tag.getAttributeValue(ATTR_NAME);
if (facet != null && oldName != null) {
for (XmlTag attr : tag.getSubTags()) {
if (attr.getName().equals(TAG_ATTR)) {
String name = attr.getAttributeValue(ATTR_NAME);
if (name != null) {
String oldAttributeName = oldName + '_' + name;
PsiField[] fields = AndroidResourceUtil.findResourceFields(facet, STYLEABLE.getName(), oldAttributeName, true);
if (fields.length > 0) {
String newAttributeName = newName + '_' + name;
for (PsiField f : fields) {
allRenames.put(f, newAttributeName);
}
}
}
}
}
}
}
}
}
}
}
private static void prepareValueResourceRenaming(PsiElement element,
String newName,
Map<PsiElement, String> allRenames,
AndroidFacet facet) {
ResourceManager manager = facet.getLocalResourceManager();
XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class);
assert tag != null;
String type = manager.getValueResourceType(tag);
assert type != null;
Project project = tag.getProject();
DomElement domElement = DomManager.getDomManager(project).getDomElement(tag);
assert domElement instanceof ResourceElement;
String name = ((ResourceElement)domElement).getName().getValue();
assert name != null;
List<ResourceElement> resources = manager.findValueResources(type, name);
for (ResourceElement resource : resources) {
XmlElement xmlElement = resource.getName().getXmlAttributeValue();
if (!element.getManager().areElementsEquivalent(element, xmlElement)) {
allRenames.put(xmlElement, newName);
}
}
PsiField[] resFields = AndroidResourceUtil.findResourceFieldsForValueResource(tag, false);
for (PsiField resField : resFields) {
String escaped = AndroidResourceUtil.getFieldNameByResourceName(newName);
allRenames.put(resField, escaped);
}
// Also rename the dependent fields, e.g. if you rename <declare-styleable name="Foo">,
// we have to rename not just R.styleable.Foo but the also R.styleable.Foo_* attributes
PsiField[] styleableFields = AndroidResourceUtil.findStyleableAttributeFields(tag, false);
if (styleableFields.length > 0) {
String tagName = tag.getName();
boolean isDeclareStyleable = tagName.equals(TAG_DECLARE_STYLEABLE);
boolean isAttr = !isDeclareStyleable && tagName.equals(TAG_ATTR) && tag.getParentTag() != null;
assert isDeclareStyleable || isAttr;
String style = isAttr ? tag.getParentTag().getAttributeValue(ATTR_NAME) : null;
for (PsiField resField : styleableFields) {
String fieldName = resField.getName();
String newAttributeName;
if (isDeclareStyleable && fieldName.startsWith(name)) {
newAttributeName = newName + fieldName.substring(name.length());
}
else if (isAttr && style != null) {
newAttributeName = style + '_' + newName;
}
else {
newAttributeName = name;
}
String escaped = AndroidResourceUtil.getFieldNameByResourceName(newAttributeName);
allRenames.put(resField, escaped);
}
}
}
private static void prepareResourceFileRenaming(PsiFile file, String newName, Map<PsiElement, String> allRenames, AndroidFacet facet) {
Project project = file.getProject();
ResourceManager manager = facet.getLocalResourceManager();
String type = manager.getFileResourceType(file);
if (type == null) return;
String name = file.getName();
if (AndroidCommonUtils.getResourceName(type, name).equals(AndroidCommonUtils.getResourceName(type, newName))) {
return;
}
List<PsiFile> resourceFiles = manager.findResourceFiles(type, AndroidCommonUtils.getResourceName(type, name), true, false);
List<PsiFile> alternativeResources = new ArrayList<PsiFile>();
for (PsiFile resourceFile : resourceFiles) {
if (!resourceFile.getManager().areElementsEquivalent(file, resourceFile) && resourceFile.getName().equals(name)) {
alternativeResources.add(resourceFile);
}
}
if (alternativeResources.size() > 0) {
int r = 0;
if (ASK) {
r = Messages.showDialog(project, message("rename.alternate.resources.question"), message("rename.dialog.title"),
new String[]{Messages.YES_BUTTON, Messages.NO_BUTTON}, 1, Messages.getQuestionIcon());
}
if (r == 0) {
for (PsiFile candidate : alternativeResources) {
allRenames.put(candidate, newName);
}
}
else {
return;
}
}
PsiField[] resFields = AndroidResourceUtil.findResourceFieldsForFileResource(file, false);
for (PsiField resField : resFields) {
String newFieldName = AndroidCommonUtils.getResourceName(type, newName);
allRenames.put(resField, AndroidResourceUtil.getFieldNameByResourceName(newFieldName));
}
}
@Override
public void renameElement(PsiElement element, final String newName, UsageInfo[] usages, @Nullable RefactoringElementListener listener)
throws IncorrectOperationException {
if (element instanceof PsiField) {
new RenameJavaVariableProcessor().renameElement(element, newName, usages, listener);
}
else {
if (element instanceof PsiNamedElement) {
super.renameElement(element, newName, usages, listener);
if (element instanceof PsiFile) {
VirtualFile virtualFile = ((PsiFile)element).getVirtualFile();
if (virtualFile != null && !LocalHistory.getInstance().isUnderControl(virtualFile)) {
DocumentReference ref = DocumentReferenceManager.getInstance().create(virtualFile);
UndoManager.getInstance(element.getProject()).nonundoableActionPerformed(ref, false);
}
}
}
else if (element instanceof XmlAttributeValue) {
new RenameXmlAttributeProcessor().renameElement(element, newName, usages, listener);
}
}
}
}