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