/* * Copyright (C) 2012 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.ide.common.res2; import static com.android.SdkConstants.ANDROID_NEW_ID_PREFIX; import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX_LEN; import static com.android.SdkConstants.ANDROID_PREFIX; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_PARENT; import static com.android.SdkConstants.ATTR_QUANTITY; import static com.android.SdkConstants.ATTR_TYPE; import static com.android.SdkConstants.ATTR_VALUE; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.ide.common.resources.ResourceResolver.ATTR_EXAMPLE; import static com.android.ide.common.resources.ResourceResolver.XLIFF_G_TAG; import static com.android.ide.common.resources.ResourceResolver.XLIFF_NAMESPACE_PREFIX; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ArrayResourceValue; import com.android.ide.common.rendering.api.AttrResourceValue; import com.android.ide.common.rendering.api.DeclareStyleableResourceValue; import com.android.ide.common.rendering.api.DensityBasedResourceValue; import com.android.ide.common.rendering.api.ItemResourceValue; import com.android.ide.common.rendering.api.PluralsResourceValue; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.StyleResourceValue; import com.android.ide.common.rendering.api.TextResourceValue; import com.android.ide.common.resources.configuration.Configurable; 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.ResourceType; import com.android.utils.XmlUtils; import com.google.common.base.Splitter; 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; /** * A resource. * * This includes the name, type, source file as a {@link ResourceFile} and an optional {@link Node} * in case of a resource coming from a value file. */ public class ResourceItem extends DataItem<ResourceFile> implements Configurable, Comparable<ResourceItem> { @NonNull private final ResourceType mType; @Nullable private Node mValue; @Nullable protected ResourceValue mResourceValue; /** * Constructs the object with a name, type and optional value. * * Note that the object is not fully usable as-is. It must be added to a ResourceFile first. * * @param name the name of the resource * @param type the type of the resource * @param value an optional Node that represents the resource value. */ public ResourceItem(@NonNull String name, @NonNull ResourceType type, @Nullable Node value) { super(name); mType = type; mValue = value; } /** * Returns the type of the resource. * * @return the type. */ @NonNull public ResourceType getType() { return mType; } /** * Returns the optional value of the resource. Can be null * * @return the value or null. */ @Nullable public Node getValue() { return mValue; } /** * Returns the optional string value of the resource. Can be null * * @return the value or null. */ @Nullable public String getValueText() { return mValue != null ? mValue.getTextContent() : null; } /** * Returns the resource item qualifiers. * @return the qualifiers */ @NonNull public String getQualifiers() { ResourceFile resourceFile = getSource(); if (resourceFile == null) { throw new RuntimeException("Cannot call getQualifier on " + toString()); } return resourceFile.getQualifiers(); } @NonNull public DataFile.FileType getSourceType() { ResourceFile resourceFile = getSource(); if (resourceFile == null) { throw new RuntimeException("Cannot call getSourceType on " + toString()); } return resourceFile.getType(); } /** * Sets the value of the resource and set its state to TOUCHED. * * @param from the resource to copy the value from. */ void setValue(@NonNull ResourceItem from) { mValue = from.mValue; setTouched(); } @Override public FolderConfiguration getConfiguration() { String qualifier = getQualifiers(); if (qualifier.isEmpty()) { return new FolderConfiguration(); } return FolderConfiguration.getConfigForQualifierString(qualifier); } /** * Returns a key for this resource. They key uniquely identifies this resource by combining * resource type, qualifiers, and name. * * If the resource has not been added to a {@link ResourceFile}, this will throw an {@link * IllegalStateException}. * * @return the key for this resource. * @throws IllegalStateException if the resource is not added to a ResourceFile */ @Override public String getKey() { if (getSource() == null) { throw new IllegalStateException( "ResourceItem.getKey called on object with no ResourceFile: " + this); } String qualifiers = getQualifiers(); String typeName = mType.getName(); if (mType == ResourceType.PUBLIC && mValue != null) { String typeAttribute = ((Element) mValue).getAttribute(ATTR_TYPE); if (typeAttribute != null) { typeName += "_" + typeAttribute; } } if (!qualifiers.isEmpty()) { return typeName + "-" + qualifiers + "/" + getName(); } return typeName + "/" + getName(); } @Override protected void wasTouched() { mResourceValue = null; } @Nullable public ResourceValue getResourceValue(boolean isFrameworks) { if (mResourceValue == null) { //noinspection VariableNotUsedInsideIf if (mValue == null) { // Density based resource value? Density density = mType == ResourceType.DRAWABLE || mType == ResourceType.MIPMAP ? getFolderDensity() : null; if (density != null) { mResourceValue = new DensityBasedResourceValue(mType, getName(), getSource().getFile().getAbsolutePath(), density, isFrameworks); } else { mResourceValue = new ResourceValue(mType, getName(), getSource().getFile().getAbsolutePath(), isFrameworks); } } else { mResourceValue = parseXmlToResourceValue(isFrameworks); } } return mResourceValue; } // TODO: We should be storing shared FolderConfiguration instances on the ResourceFiles // instead. This is a temporary fix to make rendering work properly again. @Nullable private Density getFolderDensity() { String qualifiers = getQualifiers(); if (!qualifiers.isEmpty() && qualifiers.contains("dpi")) { Iterable<String> segments = Splitter.on('-').split(qualifiers); FolderConfiguration config = FolderConfiguration.getConfigFromQualifiers(segments); if (config != null) { DensityQualifier densityQualifier = config.getDensityQualifier(); if (densityQualifier != null) { return densityQualifier.getValue(); } } } return null; } /** * Returns a formatted string usable in an XML to use for the {@link ResourceItem}. * * @param system Whether this is a system resource or a project resource. * @return a string in the format @[type]/[name] */ public String getXmlString(ResourceType type, boolean system) { if (type == ResourceType.ID /* && isDeclaredInline()*/) { return (system ? ANDROID_NEW_ID_PREFIX : NEW_ID_PREFIX) + "/" + getName(); } return (system ? ANDROID_PREFIX : PREFIX_RESOURCE_REF) + type.getName() + "/" + getName(); } /** * Compares the ResourceItem {@link #getValue()} together and returns true if they are the * same. * * @param resource The ResourceItem object to compare to. * @return true if equal */ public boolean compareValueWith(ResourceItem resource) { if (mValue != null && resource.mValue != null) { return NodeUtils.compareElementNode(mValue, resource.mValue, true); } return mValue == resource.mValue; } @Override public String toString() { return "ResourceItem{" + "mName='" + getName() + '\'' + ", mType=" + mType + ", mStatus=" + getStatus() + '}'; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } if (!super.equals(o)) { return false; } ResourceItem that = (ResourceItem) o; return mType == that.mType; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + mType.hashCode(); return result; } @Nullable private ResourceValue parseXmlToResourceValue(boolean isFrameworks) { assert mValue != null; NamedNodeMap attributes = mValue.getAttributes(); ResourceType type = getType(mValue.getLocalName(), attributes); if (type == null) { return null; } ResourceValue value; String name = getName(); switch (type) { case STYLE: String parent = getAttributeValue(attributes, ATTR_PARENT); try { value = parseStyleValue( new StyleResourceValue(type, name, parent, isFrameworks)); } catch (Throwable t) { //noinspection UseOfSystemOutOrSystemErr System.err.println("Problem parsing attribute " + name + " of type " + type + " for node " + mValue); return null; } break; case DECLARE_STYLEABLE: value = parseDeclareStyleable(new DeclareStyleableResourceValue(type, name, isFrameworks)); break; case ARRAY: value = parseArrayValue(new ArrayResourceValue(name, isFrameworks)); break; case PLURALS: value = parsePluralsValue(new PluralsResourceValue(name, isFrameworks)); break; case ATTR: value = parseAttrValue(new AttrResourceValue(type, name, isFrameworks)); break; case STRING: value = parseTextValue(new TextResourceValue(type, name, isFrameworks)); break; default: value = parseValue(new ResourceValue(type, name, isFrameworks)); break; } return value; } @Nullable private ResourceType getType(String qName, NamedNodeMap attributes) { String typeValue; // if the node is <item>, we get the type from the attribute "type" if (SdkConstants.TAG_ITEM.equals(qName)) { typeValue = getAttributeValue(attributes, ATTR_TYPE); } else { // the type is the name of the node. typeValue = qName; } return ResourceType.getEnum(typeValue); } @Nullable private static String getAttributeValue(NamedNodeMap attributes, String attributeName) { Attr attribute = (Attr) attributes.getNamedItem(attributeName); if (attribute != null) { return attribute.getValue(); } return null; } @NonNull private ResourceValue parseStyleValue(@NonNull StyleResourceValue styleValue) { NodeList children = mValue.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { NamedNodeMap attributes = child.getAttributes(); String name = getAttributeValue(attributes, 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; } ItemResourceValue resValue = new ItemResourceValue(name, isFrameworkAttr, styleValue.isFramework()); String text = getTextNode(child.getChildNodes()); resValue.setValue(ValueXmlHelper.unescapeResourceString(text, false, true)); styleValue.addItem(resValue); } } } return styleValue; } @NonNull private AttrResourceValue parseAttrValue(@NonNull AttrResourceValue attrValue) { return parseAttrValue(mValue, attrValue); } @NonNull private static AttrResourceValue parseAttrValue(@NonNull Node valueNode, @NonNull AttrResourceValue attrValue) { NodeList children = valueNode.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { NamedNodeMap attributes = child.getAttributes(); String name = getAttributeValue(attributes, ATTR_NAME); if (name != null) { String value = getAttributeValue(attributes, 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) { NodeList children = mValue.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { String text = getTextNode(child.getChildNodes()); text = ValueXmlHelper.unescapeResourceString(text, false, true); arrayValue.addElement(text); } } return arrayValue; } private ResourceValue parsePluralsValue(PluralsResourceValue value) { NodeList children = mValue.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { NamedNodeMap attributes = child.getAttributes(); String quantity = getAttributeValue(attributes, ATTR_QUANTITY); if (quantity != null) { String text = getTextNode(child.getChildNodes()); text = ValueXmlHelper.unescapeResourceString(text, false, true); value.addPlural(quantity, text); } } } return value; } @NonNull private ResourceValue parseDeclareStyleable( @NonNull DeclareStyleableResourceValue declareStyleable) { NodeList children = mValue.getChildNodes(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { NamedNodeMap attributes = child.getAttributes(); String name = getAttributeValue(attributes, 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 parseValue(@NonNull ResourceValue value) { String text = getTextNode(mValue.getChildNodes()); value.setValue(ValueXmlHelper.unescapeResourceString(text, false, true)); return value; } @NonNull private static String getTextNode(@NonNull NodeList children) { StringBuilder sb = new StringBuilder(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); short nodeType = child.getNodeType(); switch (nodeType) { case Node.ELEMENT_NODE: { Element element = (Element) child; if (XLIFF_G_TAG.equals(element.getLocalName()) && element.getNamespaceURI() != null && element.getNamespaceURI().startsWith( XLIFF_NAMESPACE_PREFIX)) { if (element.hasAttribute(ATTR_EXAMPLE)) { // <xliff:g id="number" example="7">%d</xliff:g> minutes // => "(7) minutes" String example = element.getAttribute(ATTR_EXAMPLE); sb.append('(').append(example).append(')'); continue; } else if (element.hasAttribute(ATTR_ID)) { // Step <xliff:g id="step_number">%1$d</xliff:g> // => Step ${step_number} String id = element.getAttribute(ATTR_ID); sb.append('$').append('{').append(id).append('}'); continue; } } NodeList childNodes = child.getChildNodes(); if (childNodes.getLength() > 0) { sb.append(getTextNode(childNodes)); } break; } case Node.TEXT_NODE: sb.append(child.getNodeValue()); break; case Node.CDATA_SECTION_NODE: sb.append(child.getNodeValue()); break; } } return sb.toString(); } @NonNull private TextResourceValue parseTextValue(@NonNull TextResourceValue value) { NodeList children = mValue.getChildNodes(); String text = getTextNode(children); value.setValue(ValueXmlHelper.unescapeResourceString(text, false, true)); int length = children.getLength(); if (length > 1) { boolean haveElementChildren = false; for (int i = 0; i < length; i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { haveElementChildren = true; break; } } if (haveElementChildren) { String markupText = getMarkupText(children); value.setRawXmlValue(markupText); } } return value; } @NonNull private static String getMarkupText(@NonNull NodeList children) { StringBuilder sb = new StringBuilder(); for (int i = 0, n = children.getLength(); i < n; i++) { Node child = children.item(i); short nodeType = child.getNodeType(); switch (nodeType) { case Node.ELEMENT_NODE: { Element element = (Element) child; String tagName = element.getTagName(); sb.append('<'); sb.append(tagName); NamedNodeMap attributes = element.getAttributes(); int attributeCount = attributes.getLength(); if (attributeCount > 0) { for (int j = 0; j < attributeCount; j++) { Node attribute = attributes.item(j); sb.append(' '); sb.append(attribute.getNodeName()); sb.append('=').append('"'); XmlUtils.appendXmlAttributeValue(sb, attribute.getNodeValue()); sb.append('"'); } } sb.append('>'); NodeList childNodes = child.getChildNodes(); if (childNodes.getLength() > 0) { sb.append(getMarkupText(childNodes)); } sb.append('<'); sb.append('/'); sb.append(tagName); sb.append('>'); break; } case Node.TEXT_NODE: sb.append(child.getNodeValue()); break; case Node.CDATA_SECTION_NODE: sb.append(child.getNodeValue()); break; } } return sb.toString(); } @Override public int compareTo(@NonNull ResourceItem resourceItem) { int comp = mType.compareTo(resourceItem.getType()); if (comp == 0) { comp = getName().compareTo(resourceItem.getName()); } return comp; } private boolean mIgnoredFromDiskMerge = false; public void setIgnoredFromDiskMerge(boolean ignored) { mIgnoredFromDiskMerge = ignored; } public boolean getIgnoredFromDiskMerge() { return mIgnoredFromDiskMerge; } // Used for the blob writing. // TODO: move this to ResourceMerger/Set. @Override void addExtraAttributes(Document document, Node node, String namespaceUri) { NodeUtils.addAttribute(document, node, null, ATTR_TYPE, mType.getName()); } @Override Node getDetailsXml(Document document) { return NodeUtils.adoptNode(document, mValue); } }