diff options
author | Googler <noreply@google.com> | 2016-07-14 22:31:10 +0000 |
---|---|---|
committer | Dmitry Lomov <dslomov@google.com> | 2016-07-15 13:31:04 +0000 |
commit | 3b25028750dd7a6df6777f6c70c1feae9063a630 (patch) | |
tree | ed88d83b83b6fad418199d48c146a27e35fa0782 /src/tools/android/java/com/google/devtools/build/android/xml | |
parent | e629537510a297b587d7204549dd2aae9222728e (diff) |
Record and propagate namespaces from the <resources> element correctly.
* Reduces the size of merged values.xml
* Improves correctness of merged xml
Sadly, this is also backwards compatible by allowing multiple definitions of a prefix with different namespaces.
Will be cleaned up after transition.
--
MOS_MIGRATED_REVID=127481147
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/xml')
8 files changed, 190 insertions, 27 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java index 5c9d64b637..71e24691ca 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/ArrayXmlResourceValue.java @@ -23,7 +23,6 @@ import com.google.devtools.build.android.FullyQualifiedName; import com.google.devtools.build.android.XmlResourceValue; import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; @@ -31,7 +30,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; - import javax.annotation.concurrent.Immutable; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; @@ -139,7 +137,8 @@ public class ArrayXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { return XmlResourceValues.serializeProtoDataValue( output, XmlResourceValues.newSerializableDataValueBuilder(source) @@ -147,6 +146,7 @@ public class ArrayXmlResourceValue implements XmlResourceValue { SerializeFormat.DataValueXml.newBuilder() .addAllListValue(values) .setType(SerializeFormat.DataValueXml.XmlType.ARRAY) + .putAllNamespace(namespaces.asMap()) .putAllAttribute(attributes) .setValueType(arrayType.toString()))); } @@ -181,7 +181,8 @@ public class ArrayXmlResourceValue implements XmlResourceValue { throw new IllegalArgumentException(this + " is not a combinable resource."); } - public static XmlResourceValue parseArray(XMLEventReader eventReader, StartElement start) + public static XmlResourceValue parseArray( + XMLEventReader eventReader, StartElement start, Namespaces.Collector namespacesCollector) throws XMLStreamException { List<String> values = new ArrayList<>(); for (XMLEvent element = XmlResourceValues.nextTag(eventReader); @@ -193,7 +194,8 @@ public class ArrayXmlResourceValue implements XmlResourceValue { String.format("Expected start element %s", element), element.getLocation()); } String contents = - XmlResourceValues.readContentsAsString(eventReader, element.asStartElement().getName()); + XmlResourceValues.readContentsAsString( + eventReader, element.asStartElement().getName(), namespacesCollector); values.add(contents != null ? contents : ""); } } diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java index 5686b04e72..ca7a984bd8 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/AttrXmlResourceValue.java @@ -32,7 +32,6 @@ import com.google.devtools.build.android.XmlResourceValue; import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.protobuf.InvalidProtocolBufferException; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; @@ -43,7 +42,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; - import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -331,12 +329,15 @@ public class AttrXmlResourceValue implements XmlResourceValue { @SuppressWarnings("deprecation") @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { SerializeFormat.DataValue.Builder builder = XmlResourceValues.newSerializableDataValueBuilder(source); SerializeFormat.DataValueXml.Builder xmlValueBuilder = SerializeFormat.DataValueXml.newBuilder(); - xmlValueBuilder.setType(SerializeFormat.DataValueXml.XmlType.ATTR); + xmlValueBuilder + .setType(SerializeFormat.DataValueXml.XmlType.ATTR) + .putAllNamespace(namespaces.asMap()); for (Entry<String, ResourceXmlAttrValue> entry : formats.entrySet()) { xmlValueBuilder .getMutableMappedXmlValue() diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java index b300e43851..1b56f8b0c7 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/IdXmlResourceValue.java @@ -23,12 +23,10 @@ import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.Builder; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlType; import com.google.protobuf.CodedOutputStream; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.Objects; - import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -82,8 +80,12 @@ public class IdXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { - Builder xmlValue = SerializeFormat.DataValueXml.newBuilder().setType(XmlType.ID); + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { + Builder xmlValue = + SerializeFormat.DataValueXml.newBuilder() + .setType(XmlType.ID) + .putAllNamespace(namespaces.asMap()); if (value != null) { xmlValue.setValue(value); } diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/Namespaces.java b/src/tools/android/java/com/google/devtools/build/android/xml/Namespaces.java new file mode 100644 index 0000000000..cbaabea740 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/xml/Namespaces.java @@ -0,0 +1,159 @@ +// 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.xml; + +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.android.DataResourceXml; +import com.google.devtools.build.android.XmlResourceValue; +import com.google.devtools.build.android.XmlResourceValues; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Logger; +import javax.xml.namespace.QName; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.StartElement; + +/** + * Represents a collection of xml namespaces. + * + * <p>Each <resources> can have xmlns declarations. Since the merging process generates the + * resources tag to combining multiple {@link DataResourceXml}s, the Namespaces must be tracked and + * kept with each value. + */ +public class Namespaces implements Iterable<Entry<String, String>> { + private static final Logger logger = Logger.getLogger(Namespaces.class.getCanonicalName()); + private static final Namespaces EMPTY_INSTANCE = + new Namespaces(ImmutableMap.<String, String>of()); + + /** + * Collects prefix and uri pairs from elements. + */ + public static class Collector { + private Map<String, String> prefixToUri = new HashMap<>(); + + public Namespaces toNamespaces() { + return Namespaces.from(prefixToUri); + } + + /** + * Collects all the prefix and uri pairs from a start element. + * + * <p>Since {@link Namespaces} represents top level declarations, collectFrom ignores any prefix + * that is declared on the element. Those will be handled by the {@link XmlResourceValue} + * individually. + * + * @param start The element to collect prefix and uris from. + * @return The current namespace builder. + */ + public Collector collectFrom(StartElement start) { + Iterator<Attribute> attributes = XmlResourceValues.iterateAttributesFrom(start); + Iterator<Namespace> localNamespaces = XmlResourceValues.iterateNamespacesFrom(start); + // Collect the local prefixes to make sure a prefix isn't declared locally. + Set<String> prefixes = new HashSet<>(); + while (localNamespaces.hasNext()) { + prefixes.add(localNamespaces.next().getPrefix()); + } + collectFrom(start.getName(), prefixes); + while (attributes.hasNext()) { + collectFrom(attributes.next().getName(), prefixes); + } + return this; + } + + void collectFrom(QName name, Set<String> localPrefixes) { + String prefix = name.getPrefix(); + if (!prefix.isEmpty() && !localPrefixes.contains(prefix)) { + // If the prefix exists and is not a locally declared prefix, add the prefix and uri. + prefixToUri.put(prefix, name.getNamespaceURI()); + } + } + } + + public static Collector collector() { + return new Collector(); + } + + public static Namespaces from(Map<String, String> prefixToUri) { + if (prefixToUri.isEmpty()) { + return empty(); + } + return new Namespaces(ImmutableMap.copyOf(prefixToUri)); + } + + public static Namespaces empty() { + return EMPTY_INSTANCE; + } + + private ImmutableMap<String, String> prefixToUri; + + private Namespaces(ImmutableMap<String, String> prefixToUri) { + this.prefixToUri = prefixToUri; + } + + /** Combines two {@link Namespaces} into a new instance and returns it. */ + public Namespaces union(Namespaces other) { + // No prefixes to add, return the other. + if (prefixToUri.isEmpty()) { + return other; + } + // TODO(corysmith): Issue error when prefixes are mapped to different uris. + // Keeping behavior for backwards compatibility. + Map<String, String> combinedNamespaces = new LinkedHashMap<>(); + combinedNamespaces.putAll(other.prefixToUri); + for (Entry<String, String> namespace : prefixToUri.entrySet()) { + String prefix = namespace.getKey(); + String namespaceUri = namespace.getValue(); + if (combinedNamespaces.containsKey(prefix) + && !combinedNamespaces.get(prefix).equals(namespaceUri)) { + logger.warning( + String.format( + "%s has multiple namespaces: %s and %s. Using %s." + + " This will be an error in the future.", + prefix, namespaceUri, combinedNamespaces.get(prefix), namespaceUri)); + } + combinedNamespaces.put(prefix, namespaceUri); + } + return Namespaces.from(combinedNamespaces); + } + + @Override + public Iterator<Entry<String, String>> iterator() { + return prefixToUri.entrySet().iterator(); + } + + public Map<String, String> asMap() { + return prefixToUri; + } + + @Override + public int hashCode() { + return prefixToUri.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Namespaces) { + Namespaces other = (Namespaces) obj; + return Objects.equals(prefixToUri, other.prefixToUri); + } + return false; + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java index 29b2a2608f..1e708afb1c 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/PluralXmlResourceValue.java @@ -23,13 +23,11 @@ import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlType; import com.google.protobuf.CodedOutputStream; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.Map.Entry; import java.util.Objects; - import javax.annotation.concurrent.Immutable; import javax.xml.namespace.QName; @@ -128,7 +126,8 @@ public class PluralXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { SerializeFormat.DataValue.Builder builder = XmlResourceValues.newSerializableDataValueBuilder(source); SerializeFormat.DataValue value = @@ -137,6 +136,7 @@ public class PluralXmlResourceValue implements XmlResourceValue { builder .getXmlValueBuilder() .setType(XmlType.PLURAL) + .putAllNamespace(namespaces.asMap()) .putAllAttribute(attributes) .putAllMappedStringValue(values)) .build(); diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java index 3e308a1fab..ef4e822b4f 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java @@ -13,6 +13,7 @@ // limitations under the License. package com.google.devtools.build.android.xml; +import com.android.resources.ResourceType; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.android.AndroidDataWritingVisitor; @@ -22,15 +23,11 @@ import com.google.devtools.build.android.XmlResourceValue; import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.Builder; - -import com.android.resources.ResourceType; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.Arrays; import java.util.Objects; - import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.xml.namespace.QName; @@ -214,12 +211,14 @@ public class SimpleXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { SerializeFormat.DataValue.Builder builder = XmlResourceValues.newSerializableDataValueBuilder(source); Builder xmlValueBuilder = builder .getXmlValueBuilder() + .putAllNamespace(namespaces.asMap()) .setType(SerializeFormat.DataValueXml.XmlType.SIMPLE) // TODO(corysmith): Find a way to avoid writing strings to the serialized format // it's inefficient use of space and costs more when deserializing. diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java index c3edb182e4..60abda34fe 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleXmlResourceValue.java @@ -23,14 +23,12 @@ import com.google.devtools.build.android.XmlResourceValue; import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlType; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; - import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -107,10 +105,12 @@ public class StyleXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { SerializeFormat.DataValueXml.Builder xmlValueBuilder = SerializeFormat.DataValueXml.newBuilder() .setType(XmlType.STYLE) + .putAllNamespace(namespaces.asMap()) .putAllMappedStringValue(values); if (parent != null) { xmlValueBuilder.setValue(parent); diff --git a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java index 095ff7a1cb..82bf56584a 100644 --- a/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java +++ b/src/tools/android/java/com/google/devtools/build/android/xml/StyleableXmlResourceValue.java @@ -26,7 +26,6 @@ import com.google.devtools.build.android.XmlResourceValue; import com.google.devtools.build.android.XmlResourceValues; import com.google.devtools.build.android.proto.SerializeFormat; import com.google.devtools.build.android.proto.SerializeFormat.DataValueXml.XmlType; - import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; @@ -35,7 +34,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; - import javax.annotation.concurrent.Immutable; /** @@ -138,13 +136,15 @@ public class StyleableXmlResourceValue implements XmlResourceValue { } @Override - public int serializeTo(Path source, OutputStream output) throws IOException { + public int serializeTo(Path source, Namespaces namespaces, OutputStream output) + throws IOException { return XmlResourceValues.serializeProtoDataValue( output, XmlResourceValues.newSerializableDataValueBuilder(source) .setXmlValue( SerializeFormat.DataValueXml.newBuilder() .setType(XmlType.STYLEABLE) + .putAllNamespace(namespaces.asMap()) .addAllReferences( Iterables.transform(attrs.entrySet(), FULLY_QUALIFIED_NAME_TO_DATA_KEY)))); } |