/* * 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(); } } } }