blob: c51636f61bde96a479a5480b3814bba238494651 [file] [log] [blame]
// 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.android.SdkConstants;
import com.android.resources.ResourceType;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.devtools.build.android.AndroidDataWritingVisitor;
import com.google.devtools.build.android.AndroidResourceSymbolSink;
import com.google.devtools.build.android.DataSource;
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.util.EnumMap;
import java.util.Map;
import java.util.Objects;
/**
* Represents an Android resource <public> xml tag.
*
* <p>This is used to declare a resource public and reserve a fixed ID for a resource. It is
* generally undocumented (update this if we ever get a doc), but used heavily by the android
* framework resources. One description of it is at the android <a
* href="http://tools.android.com/tech-docs/private-resources">tools site</a>. Public tags can be
* defined in any xml file in the values folder. <code>
* &lt;resources&gt;
* &lt;string name="mypublic_string"&gt; Pub &lt;/string&gt;
* &lt;public name="mypublic_string" type="string" id="0x7f050004" /&gt;
* &lt;string name="myother_string"&gt; the others &lt;/string&gt;
* &lt;public name="myother_string" type="string" /&gt;
* &lt;/resources&gt;
* </code> The "id" attribute is optional if an earlier public tag has already specified an "id"
* attribute. In such cases, ID assignment will continue from the previous reserved ID.
*/
public class PublicXmlResourceValue implements XmlResourceValue {
private final Map<ResourceType, Optional<Integer>> typeToId;
private static final String MISSING_ID_VALUE = "";
private PublicXmlResourceValue(Map<ResourceType, Optional<Integer>> typeToId) {
this.typeToId = typeToId;
}
public static PublicXmlResourceValue of(Map<ResourceType, Optional<Integer>> typeToId) {
return new PublicXmlResourceValue(typeToId);
}
public static XmlResourceValue create(ResourceType type, Optional<Integer> id) {
Map<ResourceType, Optional<Integer>> map = new EnumMap<>(ResourceType.class);
map.put(type, id);
return new PublicXmlResourceValue(map);
}
@Override
public void write(
FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) {
for (Map.Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) {
Integer value = entry.getValue().orNull();
mergedDataWriter
.define(key)
.derivedFrom(source)
.startTag(ResourceType.PUBLIC.getName())
.named(key)
.attribute(SdkConstants.ATTR_TYPE)
.setTo(entry.getKey().toString())
.optional()
.attribute(SdkConstants.ATTR_ID)
.setTo(value == null ? null : "0x" + Integer.toHexString(value))
.closeUnaryTag()
.save();
}
}
@Override
public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) {
for (Map.Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) {
sink.acceptPublicResource(entry.getKey(), key.name(), entry.getValue());
}
}
@Override
public int hashCode() {
return Objects.hash(typeToId);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PublicXmlResourceValue)) {
return false;
}
PublicXmlResourceValue other = (PublicXmlResourceValue) obj;
return Objects.equals(typeToId, other.typeToId);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(getClass()).add("typeToId: ", typeToId).toString();
}
@SuppressWarnings("deprecation")
public static XmlResourceValue from(SerializeFormat.DataValueXml proto) {
Map<String, String> protoValues = proto.getMappedStringValue();
ImmutableMap.Builder<ResourceType, Optional<Integer>> typeToId = ImmutableMap.builder();
for (Map.Entry<String, String> entry : protoValues.entrySet()) {
ResourceType type = ResourceType.getEnum(entry.getKey());
Preconditions.checkNotNull(type);
Optional<Integer> id =
MISSING_ID_VALUE.equals(entry.getValue())
? Optional.<Integer>absent()
: Optional.of(Integer.decode(entry.getValue()));
typeToId.put(type, id);
}
return of(typeToId.build());
}
public static XmlResourceValue from(ResourceType resourceType, int id) {
ImmutableMap.Builder<ResourceType, Optional<Integer>> typeToId = ImmutableMap.builder();
typeToId.put(resourceType, Optional.of(id));
return of(typeToId.build());
}
@Override
public int serializeTo(int sourceId, Namespaces namespaces, OutputStream output)
throws IOException {
Map<String, String> assignments = Maps.newLinkedHashMapWithExpectedSize(typeToId.size());
for (Map.Entry<ResourceType, Optional<Integer>> entry : typeToId.entrySet()) {
Optional<Integer> value = entry.getValue();
String stringValue = value.isPresent() ? value.get().toString() : MISSING_ID_VALUE;
assignments.put(entry.getKey().toString(), stringValue);
}
SerializeFormat.DataValue.Builder builder =
XmlResourceValues.newSerializableDataValueBuilder(sourceId);
builder.setXmlValue(
builder
.getXmlValueBuilder()
.setType(SerializeFormat.DataValueXml.XmlType.PUBLIC)
.putAllNamespace(namespaces.asMap())
.putAllMappedStringValue(assignments));
return XmlResourceValues.serializeProtoDataValue(output, builder);
}
@Override
public XmlResourceValue combineWith(XmlResourceValue value) {
if (!(value instanceof PublicXmlResourceValue)) {
throw new IllegalArgumentException(value + "is not combinable with " + this);
}
PublicXmlResourceValue other = (PublicXmlResourceValue) value;
Map<ResourceType, Optional<Integer>> combined = new EnumMap<>(ResourceType.class);
combined.putAll(typeToId);
for (Map.Entry<ResourceType, Optional<Integer>> entry : other.typeToId.entrySet()) {
Optional<Integer> existing = combined.get(entry.getKey());
if (existing != null && !existing.equals(entry.getValue())) {
throw new IllegalArgumentException(
String.format(
"Public resource of type %s assigned two different id values 0x%x and 0x%x",
entry.getKey(), existing.orNull(), entry.getValue().orNull()));
}
combined.put(entry.getKey(), entry.getValue());
}
return of(combined);
}
@Override
public int compareMergePriorityTo(XmlResourceValue value) {
return 0;
}
@Override
public String asConflictStringWith(DataSource source) {
return source.asConflictString();
}
}