// Copyright 2016 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.android; import static com.android.resources.ResourceType.DECLARE_STYLEABLE; import static com.android.resources.ResourceType.ID; import static com.android.resources.ResourceType.PUBLIC; import com.android.resources.ResourceType; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.FullyQualifiedName.Factory; import com.google.devtools.build.android.FullyQualifiedName.VirtualType; import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml; import com.google.devtools.build.android.xml.ArrayXmlResourceValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue; import com.google.devtools.build.android.xml.IdXmlResourceValue; import com.google.devtools.build.android.xml.Namespaces; import com.google.devtools.build.android.xml.PluralXmlResourceValue; import com.google.devtools.build.android.xml.PublicXmlResourceValue; import com.google.devtools.build.android.xml.ResourcesAttribute; import com.google.devtools.build.android.xml.SimpleXmlResourceValue; import com.google.devtools.build.android.xml.StyleXmlResourceValue; import com.google.devtools.build.android.xml.StyleableXmlResourceValue; import com.google.protobuf.InvalidProtocolBufferException; import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Iterator; import java.util.Objects; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; /** * Represents an Android Resource defined in the xml and value folder. * * <p> * Basically, if the resource is defined inside a <resources> tag, this class will handle it. * Layouts are treated separately as they don't declare anything besides ids. */ public class DataResourceXml implements DataResource { /** * Parses xml resources from a Path to the provided overwritable and combining collections. * * <p>This method is a bit tricky in the service of performance -- creating several collections * and merging them was more expensive than writing to mutable collections directly. * * @param xmlInputFactory Used to create an XMLEventReader from the supplied resource path. * @param path The path to the xml resource to be parsed. * @param fqnFactory Used to create {@link FullyQualifiedName}s from the resource names. * @param overwritingConsumer A consumer for overwritable {@link DataResourceXml}s. * @param combiningConsumer A consumer for combining {@link DataResourceXml}s. * @throws XMLStreamException Thrown with the resource format is invalid. * @throws FactoryConfigurationError Thrown with the {@link XMLInputFactory} is misconfigured. * @throws IOException Thrown when there is an error reading a file. */ public static void parse( XMLInputFactory xmlInputFactory, Path path, Factory fqnFactory, KeyValueConsumer<DataKey, DataResource> overwritingConsumer, KeyValueConsumer<DataKey, DataResource> combiningConsumer) throws XMLStreamException, FactoryConfigurationError, IOException { XMLEventReader eventReader = xmlInputFactory.createXMLEventReader( new BufferedInputStream(Files.newInputStream(path)), StandardCharsets.UTF_8.toString()); try { // TODO(corysmith): Make the xml parsing more readable. for (StartElement resources = XmlResourceValues.moveToResources(eventReader); resources != null; resources = XmlResourceValues.moveToResources(eventReader)) { // Record attributes on the <resources> tag. Iterator<Attribute> attributes = XmlResourceValues.iterateAttributesFrom(resources); while (attributes.hasNext()) { Attribute attribute = attributes.next(); Namespaces namespaces = Namespaces.from(attribute.getName()); String attributeName = attribute.getName().getNamespaceURI().isEmpty() ? attribute.getName().getLocalPart() : attribute.getName().getPrefix() + ":" + attribute.getName().getLocalPart(); FullyQualifiedName fqn = fqnFactory.create( VirtualType.RESOURCES_ATTRIBUTE, attribute.getName().toString()); ResourcesAttribute resourceAttribute = ResourcesAttribute.of(fqn, attributeName, attribute.getValue()); DataResourceXml resource = DataResourceXml.createWithNamespaces( path, resourceAttribute, namespaces); if (resourceAttribute.isCombining()) { combiningConsumer.consume(fqn, resource); } else { overwritingConsumer.consume(fqn, resource); } } // Process resource declarations. for (StartElement start = XmlResourceValues.findNextStart(eventReader); start != null; start = XmlResourceValues.findNextStart(eventReader)) { Namespaces.Collector namespacesCollector = Namespaces.collector(); if (XmlResourceValues.isEatComment(start) || XmlResourceValues.isSkip(start)) { continue; } ResourceType resourceType = getResourceType(start); if (resourceType == null) { throw new XMLStreamException( path + " contains an unrecognized resource type: " + start, start.getLocation()); } if (resourceType == DECLARE_STYLEABLE) { // Styleables are special, as they produce multiple overwrite and combining values, // so we let the value handle the assignments. XmlResourceValues.parseDeclareStyleable( fqnFactory, path, overwritingConsumer, combiningConsumer, eventReader, start); } else { // Of simple resources, only IDs and Public are combining. KeyValueConsumer<DataKey, DataResource> consumer = (resourceType == ID || resourceType == PUBLIC) ? combiningConsumer : overwritingConsumer; String elementName = XmlResourceValues.getElementName(start); if (elementName == null) { throw new XMLStreamException( String.format("resource name is required for %s", resourceType), start.getLocation()); } FullyQualifiedName key = fqnFactory.create(resourceType, elementName); XmlResourceValue xmlResourceValue = parseXmlElements(resourceType, eventReader, start, namespacesCollector); consumer.consume( key, DataResourceXml.createWithNamespaces( path, xmlResourceValue, namespacesCollector.toNamespaces())); } } } } catch (XMLStreamException e) { throw new XMLStreamException(path + ": " + e.getMessage(), e.getLocation(), e); } catch (RuntimeException e) { throw new RuntimeException("Error parsing " + path, e); } } @SuppressWarnings("deprecation") // TODO(corysmith): Update proto to use get<>Map public static DataValue from(SerializeFormat.DataValue protoValue, DataSource source) throws InvalidProtocolBufferException { DataValueXml xmlValue = protoValue.getXmlValue(); return createWithNamespaces( source, valueFromProto(xmlValue), Namespaces.from(xmlValue.getNamespace())); } private static XmlResourceValue valueFromProto(SerializeFormat.DataValueXml proto) throws InvalidProtocolBufferException { Preconditions.checkArgument(proto.hasType()); switch (proto.getType()) { case ARRAY: return ArrayXmlResourceValue.from(proto); case SIMPLE: return SimpleXmlResourceValue.from(proto); case ATTR: return AttrXmlResourceValue.from(proto); case ID: return IdXmlResourceValue.of(); case PLURAL: return PluralXmlResourceValue.from(proto); case PUBLIC: return PublicXmlResourceValue.from(proto); case STYLE: return StyleXmlResourceValue.from(proto); case STYLEABLE: return StyleableXmlResourceValue.from(proto); case RESOURCES_ATTRIBUTE: return ResourcesAttribute.from(proto); default: throw new IllegalArgumentException(); } } private static XmlResourceValue parseXmlElements( ResourceType resourceType, XMLEventReader eventReader, StartElement start, Namespaces.Collector namespacesCollector) throws XMLStreamException { // Handle ids first, as they are a special kind of item. if (resourceType == ID) { return XmlResourceValues.parseId(eventReader, start, namespacesCollector); } // Handle item stubs. if (XmlResourceValues.isItem(start)) { return XmlResourceValues.parseSimple(eventReader, resourceType, start, namespacesCollector); } switch (resourceType) { case STYLE: return XmlResourceValues.parseStyle(eventReader, start); case ARRAY: return ArrayXmlResourceValue.parseArray(eventReader, start, namespacesCollector); case PLURALS: return XmlResourceValues.parsePlurals(eventReader, start, namespacesCollector); case ATTR: return XmlResourceValues.parseAttr(eventReader, start); case PUBLIC: return XmlResourceValues.parsePublic(eventReader, start, namespacesCollector); case LAYOUT: case DIMEN: case STRING: case BOOL: case COLOR: case FRACTION: case INTEGER: case DRAWABLE: case ANIM: case ANIMATOR: case DECLARE_STYLEABLE: case INTERPOLATOR: case MENU: case MIPMAP: case RAW: case STYLEABLE: case TRANSITION: case XML: return XmlResourceValues.parseSimple(eventReader, resourceType, start, namespacesCollector); default: throw new XMLStreamException( String.format("Unhandled resourceType %s", resourceType), start.getLocation()); } } private static ResourceType getResourceType(StartElement start) { if (XmlResourceValues.isItem(start)) { return ResourceType.getEnum(XmlResourceValues.getElementType(start)); } return ResourceType.getEnum(start.getName().getLocalPart()); } private final DataSource source; private final XmlResourceValue xml; private final Namespaces namespaces; private DataResourceXml(DataSource source, XmlResourceValue xmlValue, Namespaces namespaces) { this.source = source; this.xml = xmlValue; this.namespaces = namespaces; } public static DataResourceXml createWithNoNamespace(Path sourcePath, XmlResourceValue xml) { return createWithNamespaces(sourcePath, xml, ImmutableMap.<String, String>of()); } public static DataResourceXml createWithNamespaces( Path sourcePath, XmlResourceValue xml, ImmutableMap<String, String> prefixToUri) { return createWithNamespaces(sourcePath, xml, Namespaces.from(prefixToUri)); } public static DataResourceXml createWithNoNamespace(DataSource source, XmlResourceValue xml) { return new DataResourceXml(source, xml, Namespaces.empty()); } public static DataResourceXml createWithNamespaces( DataSource source, XmlResourceValue xml, Namespaces namespaces) { return new DataResourceXml(source, xml, namespaces); } public static DataResourceXml createWithNamespaces( Path sourcePath, XmlResourceValue xml, Namespaces namespaces) { return createWithNamespaces(DataSource.of(sourcePath), xml, namespaces); } @Override public DataSource source() { return source; } @Override public int hashCode() { return Objects.hash(source, xml, namespaces); } @Override public boolean equals(Object obj) { if (!(obj instanceof DataResourceXml)) { return false; } DataResourceXml other = (DataResourceXml) obj; return Objects.equals(source, other.source) && Objects.equals(xml, other.xml) && Objects.equals(namespaces, other.namespaces); } @Override public String toString() { return MoreObjects.toStringHelper(getClass()) .add("source", source) .add("xml", xml) .add("namespaces", namespaces) .toString(); } @Override public void writeResource(FullyQualifiedName key, AndroidDataWritingVisitor mergedDataWriter) { mergedDataWriter.defineNamespacesFor(key, namespaces); xml.write(key, source, mergedDataWriter); } @Override public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) { xml.writeResourceToClass(key, sink); } @Override public int serializeTo(DataKey key, DataSourceTable sourceTable, OutputStream outStream) throws IOException { return xml.serializeTo(sourceTable.getSourceId(source), namespaces, outStream); } // TODO(corysmith): Clean up all the casting. The type structure is unclean. @Override public DataResource combineWith(DataResource resource) { if (!(resource instanceof DataResourceXml)) { throw new IllegalArgumentException(resource + " is not combinable with " + this); } DataResourceXml xmlResource = (DataResourceXml) resource; return createWithNamespaces( source.combine(xmlResource.source), xml.combineWith(xmlResource.xml), namespaces.union(xmlResource.namespaces)); } @Override public DataResource overwrite(DataResource resource) { if (equals(resource)) { return this; } return createWithNamespaces(source.overwrite(resource.source()), xml, namespaces); } @Override public DataValue update(DataSource source) { return createWithNamespaces(source, xml, namespaces); } @Override public String asConflictString() { return source.asConflictString(); } }