/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.android.tools.idea.rendering;
import com.android.annotations.NonNull;
import com.android.ide.common.rendering.api.*;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.res2.ValueXmlHelper;
import com.android.ide.common.resources.configuration.DensityQualifier;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.google.common.base.Splitter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Computable;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import com.intellij.psi.xml.XmlTokenType;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.ResourceResolver.*;
public class PsiResourceItem extends ResourceItem {
private final XmlTag myTag;
private PsiFile myFile;
PsiResourceItem(@NonNull String name, @NonNull ResourceType type, @Nullable XmlTag tag, @NonNull PsiFile file) {
super(name, type, null);
myTag = tag;
myFile = file;
}
@Override
public FolderConfiguration getConfiguration() {
PsiResourceFile source = (PsiResourceFile)super.getSource();
// Temporary safety workaround
if (source == null) {
if (myFile != null) {
PsiDirectory parent = myFile.getParent();
if (parent != null) {
String name = parent.getName();
FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
if (configuration != null) {
return configuration;
}
}
}
String qualifiers = getQualifiers();
if (qualifiers.isEmpty()) {
return new FolderConfiguration();
}
FolderConfiguration fromQualifiers = FolderConfiguration.getConfigFromQualifiers(Splitter.on('-').split(qualifiers));
if (fromQualifiers == null) {
return new FolderConfiguration();
}
return fromQualifiers;
}
return source.getFolderConfiguration();
}
@Nullable
@Override
public ResourceFile getSource() {
ResourceFile source = super.getSource();
// Temporary safety workaround
if (source == null && myFile != null && myFile.getParent() != null) {
PsiDirectory parent = myFile.getParent();
if (parent != null) {
String name = parent.getName();
ResourceFolderType folderType = ResourceFolderType.getFolderType(name);
FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(name);
int index = name.indexOf('-');
String qualifiers = index == -1 ? "" : name.substring(index + 1);
source = new PsiResourceFile(myFile, Collections.<ResourceItem>singletonList(this), qualifiers, folderType,
configuration);
setSource(source);
}
}
return source;
}
@Nullable
@Override
public ResourceValue getResourceValue(boolean isFrameworks) {
if (mResourceValue == null) {
//noinspection VariableNotUsedInsideIf
if (myTag == null) {
// Density based resource value?
ResourceType type = getType();
Density density = type == ResourceType.DRAWABLE ? getFolderDensity() : null;
if (density != null) {
mResourceValue = new DensityBasedResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), density, isFrameworks);
} else {
mResourceValue = new ResourceValue(type, getName(), getSource().getFile().getAbsolutePath(), isFrameworks);
}
} else {
mResourceValue = parseXmlToResourceValue(isFrameworks);
}
}
return mResourceValue;
}
@Nullable
private Density getFolderDensity() {
FolderConfiguration configuration = getConfiguration();
if (configuration != null) {
DensityQualifier densityQualifier = configuration.getDensityQualifier();
if (densityQualifier != null) {
return densityQualifier.getValue();
}
}
return null;
}
@Nullable
private ResourceValue parseXmlToResourceValue(boolean isFrameworks) {
assert myTag != null;
if (!myTag.isValid()) {
return null;
}
ResourceType type = getType();
String name = getName();
ResourceValue value;
switch (type) {
case STYLE:
String parent = getAttributeValue(myTag, ATTR_PARENT);
value = parseStyleValue(new StyleResourceValue(type, name, parent, isFrameworks));
break;
case DECLARE_STYLEABLE:
//noinspection deprecation
value = parseDeclareStyleable(new DeclareStyleableResourceValue(type, name, isFrameworks));
break;
case ATTR:
value = parseAttrValue(new AttrResourceValue(type, name, isFrameworks));
break;
case ARRAY:
value = parseArrayValue(new ArrayResourceValue(name, isFrameworks) {
// Allow the user to specify a specific element to use via tools:index
@Override
protected int getDefaultIndex() {
String index = myTag.getAttributeValue(ATTR_INDEX, TOOLS_URI);
if (index != null) {
return Integer.parseInt(index);
}
return super.getDefaultIndex();
}
});
break;
case PLURALS:
value = parsePluralsValue(new PluralsResourceValue(name, isFrameworks) {
// Allow the user to specify a specific quantity to use via tools:quantity
@Override
public String getValue() {
String quantity = myTag.getAttributeValue(ATTR_QUANTITY, TOOLS_URI);
if (quantity != null) {
String value = getValue(quantity);
if (value != null) {
return value;
}
}
return super.getValue();
}
});
break;
case STRING:
value = parseTextValue(new PsiTextResourceValue(type, name, isFrameworks));
break;
default:
value = parseValue(new ResourceValue(type, name, isFrameworks));
break;
}
return value;
}
@Nullable
private static String getAttributeValue(XmlTag tag, String attributeName) {
return tag.getAttributeValue(attributeName);
}
@SuppressWarnings("deprecation") // support for deprecated (but supported) API
@NonNull
private ResourceValue parseDeclareStyleable(@NonNull DeclareStyleableResourceValue declareStyleable) {
assert myTag != null;
for (XmlTag child : myTag.getSubTags()) {
String name = getAttributeValue(child, ATTR_NAME);
if (name != null) {
// is the attribute in the android namespace?
boolean isFrameworkAttr = declareStyleable.isFramework();
if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
isFrameworkAttr = true;
}
AttrResourceValue attr = parseAttrValue(child, new AttrResourceValue(ResourceType.ATTR, name, isFrameworkAttr));
declareStyleable.addValue(attr);
}
}
return declareStyleable;
}
@NonNull
private ResourceValue parseStyleValue(@NonNull StyleResourceValue styleValue) {
assert myTag != null;
for (XmlTag child : myTag.getSubTags()) {
String name = getAttributeValue(child, ATTR_NAME);
if (name != null) {
// is the attribute in the android namespace?
boolean isFrameworkAttr = styleValue.isFramework();
if (name.startsWith(ANDROID_NS_NAME_PREFIX)) {
name = name.substring(ANDROID_NS_NAME_PREFIX_LEN);
isFrameworkAttr = true;
}
ResourceValue resValue = new ResourceValue(null, name, styleValue.isFramework());
resValue.setValue(ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true));
styleValue.addValue(resValue, isFrameworkAttr);
}
}
return styleValue;
}
@NonNull
private AttrResourceValue parseAttrValue(@NonNull AttrResourceValue attrValue) {
assert myTag != null;
return parseAttrValue(myTag, attrValue);
}
@NonNull
private static AttrResourceValue parseAttrValue(@NonNull XmlTag myTag, @NonNull AttrResourceValue attrValue) {
for (XmlTag child : myTag.getSubTags()) {
String name = getAttributeValue(child, ATTR_NAME);
if (name != null) {
String value = getAttributeValue(child, ATTR_VALUE);
if (value != null) {
try {
// Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we
// use Long.decode instead.
attrValue.addValue(name, (int)(long)Long.decode(value));
} catch (NumberFormatException e) {
// pass, we'll just ignore this value
}
}
}
}
return attrValue;
}
private ResourceValue parseArrayValue(ArrayResourceValue arrayValue) {
assert myTag != null;
for (XmlTag child : myTag.getSubTags()) {
String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
arrayValue.addElement(text);
}
return arrayValue;
}
private ResourceValue parsePluralsValue(PluralsResourceValue value) {
assert myTag != null;
for (XmlTag child : myTag.getSubTags()) {
String quantity = child.getAttributeValue(ATTR_QUANTITY);
if (quantity != null) {
String text = ValueXmlHelper.unescapeResourceString(getTextContent(child), true, true);
value.addPlural(quantity, text);
}
}
return value;
}
@NonNull
private ResourceValue parseValue(@NonNull ResourceValue value) {
assert myTag != null;
String text = getTextContent(myTag);
text = ValueXmlHelper.unescapeResourceString(text, true, true);
value.setValue(text);
return value;
}
/**
* Returns the text content of a given tag
*/
public static String getTextContent(@NonNull XmlTag tag) {
// We can't just use tag.getValue().getTrimmedText() here because we need to remove
// intermediate elements such as <xliff> text:
// TODO: Make sure I correct handle HTML content for XML items in <string> nodes!
// For example, for the following string we want to compute "Share with %s":
// <string name="share">Share with <xliff:g id="application_name" example="Bluetooth">%s</xliff:g></string>
XmlTag[] subTags = tag.getSubTags();
XmlText[] textElements = tag.getValue().getTextElements();
if (subTags.length == 0) {
if (textElements.length == 1) {
return getXmlTextValue(textElements[0]);
} else if (textElements.length == 0) {
return "";
}
}
StringBuilder sb = new StringBuilder(40);
appendText(sb, tag);
return sb.toString();
}
@NonNull
private PsiTextResourceValue parseTextValue(@NonNull PsiTextResourceValue value) {
assert myTag != null;
String text = getTextContent(myTag);
text = ValueXmlHelper.unescapeResourceString(text, true, true);
value.setValue(text);
return value;
}
private static String getXmlTextValue(XmlText element) {
PsiElement current = element.getFirstChild();
if (current != null) {
if (current.getNextSibling() != null) {
StringBuilder sb = new StringBuilder();
for (; current != null; current = current.getNextSibling()) {
IElementType type = current.getNode().getElementType();
if (type == XmlElementType.XML_CDATA) {
PsiElement[] children = current.getChildren();
if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
sb.append(children[1].getText());
}
continue;
}
sb.append(current.getText());
}
return sb.toString();
} else if (current.getNode().getElementType() == XmlElementType.XML_CDATA) {
PsiElement[] children = current.getChildren();
if (children.length == 3) { // XML_CDATA_START, XML_DATA_CHARACTERS, XML_CDATA_END
assert children[1].getNode().getElementType() == XmlTokenType.XML_DATA_CHARACTERS;
return children[1].getText();
}
}
}
return element.getText();
}
private static void appendText(@NonNull StringBuilder sb, @NonNull XmlTag tag) {
PsiElement[] children = tag.getChildren();
for (PsiElement child : children) {
if (child instanceof XmlText) {
XmlText text = (XmlText)child;
sb.append(getXmlTextValue(text));
} else if (child instanceof XmlTag) {
XmlTag childTag = (XmlTag)child;
// xliff support
if (XLIFF_G_TAG.equals(childTag.getLocalName()) && childTag.getNamespace().startsWith(XLIFF_NAMESPACE_PREFIX)) {
String example = childTag.getAttributeValue(ATTR_EXAMPLE);
if (example != null) {
// <xliff:g id="number" example="7">%d</xliff:g> minutes => "(7) minutes"
sb.append('(').append(example).append(')');
continue;
} else {
String id = childTag.getAttributeValue(ATTR_ID);
if (id != null) {
// Step <xliff:g id="step_number">%1$d</xliff:g> => Step ${step_number}
sb.append('$').append('{').append(id).append('}');
continue;
}
}
}
appendText(sb, childTag);
}
}
}
@NonNull
PsiFile getPsiFile() {
return myFile;
}
/** Clears the cached value, if any, and returns true if the value was cleared */
public boolean recomputeValue() {
if (mResourceValue != null) {
// Force recompute in getResourceValue
mResourceValue = null;
return true;
} else {
return false;
}
}
@Nullable
public XmlTag getTag() {
return myTag;
}
@Override
public boolean equals(Object o) {
// Only reference equality; we need to be able to distinguish duplicate elements which can happen during editing
// for incremental updating to handle temporarily aliasing items.
return this == o;
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return super.toString() + ": " + (myTag != null ? getTextContent(myTag) : "null");
}
private class PsiTextResourceValue extends TextResourceValue {
public PsiTextResourceValue(ResourceType type, String name, boolean isFramework) {
super(type, name, isFramework);
}
@Override
public String getRawXmlValue() {
if (myTag != null && myTag.isValid()) {
if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
return myTag.getValue().getText();
}
});
}
return myTag.getValue().getText();
}
else {
return getValue();
}
}
}
}