aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/xml/SimpleXmlResourceValue.java
blob: e2d5caf78454a68f16a612b1e7991c13ffe25bb1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
// 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.aapt.Resources.Item;
import com.android.aapt.Resources.StyledString;
import com.android.aapt.Resources.StyledString.Span;
import com.android.aapt.Resources.Value;
import com.android.resources.ResourceType;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.google.common.xml.XmlEscapers;
import com.google.devtools.build.android.AndroidDataWritingVisitor;
import com.google.devtools.build.android.AndroidDataWritingVisitor.StartTag;
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 com.google.devtools.build.android.proto.SerializeFormat.DataValueXml;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.namespace.QName;

/**
 * Represents a simple Android resource xml value.
 *
 * <p>
 * There is a class of resources that are simple name/value pairs: string
 * (http://developer.android.com/guide/topics/resources/string-resource.html), bool
 * (http://developer.android.com/guide/topics/resources/more-resources.html#Bool), color
 * (http://developer.android.com/guide/topics/resources/more-resources.html#Color), and dimen
 * (http://developer.android.com/guide/topics/resources/more-resources.html#Dimension). These are
 * defined in xml as &lt;<em>resource type</em> name="<em>name</em>" value="<em>value</em>"&gt;. In
 * the interest of keeping the parsing svelte, these are represented by a single class.
 */
@Immutable
public class SimpleXmlResourceValue implements XmlResourceValue {
  static final QName TAG_BOOL = QName.valueOf("bool");
  static final QName TAG_COLOR = QName.valueOf("color");
  static final QName TAG_DIMEN = QName.valueOf("dimen");
  static final QName TAG_DRAWABLE = QName.valueOf("drawable");
  static final QName TAG_FRACTION = QName.valueOf("fraction");
  static final QName TAG_INTEGER = QName.valueOf("integer");
  static final QName TAG_ITEM = QName.valueOf("item");
  static final QName TAG_LAYOUT = QName.valueOf("layout");
  static final QName TAG_MENU = QName.valueOf("menu");
  static final QName TAG_MIPMAP = QName.valueOf("mipmap");
  static final QName TAG_PUBLIC = QName.valueOf("public");
  static final QName TAG_RAW = QName.valueOf("raw");
  static final QName TAG_STRING = QName.valueOf("string");

  /** Provides an enumeration resource type and simple value validation. */
  public enum Type {
    BOOL(TAG_BOOL) {
      @Override
      public boolean validate(String value) {
        final String cleanValue = value.toLowerCase().trim();
        return "true".equals(cleanValue) || "false".equals(cleanValue);
      }
    },
    COLOR(TAG_COLOR) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the hex color.
        return true;
      }
    },
    DIMEN(TAG_DIMEN) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the dimension type.
        return true;
      }
    },
    DRAWABLE(TAG_DRAWABLE) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the drawable type.
        return true;
      }
    },
    FRACTION(TAG_FRACTION) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the fraction type.
        return true;
      }
    },
    INTEGER(TAG_INTEGER) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the integer type.
        return true;
      }
    },
    ITEM(TAG_ITEM) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the item type.
        return true;
      }
    },
    LAYOUT(TAG_LAYOUT) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the layout type.
        return true;
      }
    },
    MENU(TAG_MENU) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the menu type.
        return true;
      }
    },
    MIPMAP(TAG_MIPMAP) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the mipmap type.
        return true;
      }
    },
    PUBLIC(TAG_PUBLIC) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the public type.
        return true;
      }
    },
    RAW(TAG_RAW) {
      @Override
      public boolean validate(String value) {
        // TODO(corysmith): Validate the raw type.
        return true;
      }
    },
    STRING(TAG_STRING) {
      @Override
      public boolean validate(String value) {
        return true;
      }
    };
    private final QName tagName;

    Type(QName tagName) {
      this.tagName = tagName;
    }

    abstract boolean validate(String value);

    public static Type from(ResourceType resourceType) {
      for (Type valueType : values()) {
        if (valueType.tagName.getLocalPart().equals(resourceType.getName())) {
          return valueType;
        } else if (resourceType.getName().equalsIgnoreCase(valueType.name())) {
          return valueType;
        }
      }
      throw new IllegalArgumentException(
          String.format(
              "%s resource type not found in available types: %s",
              resourceType,
              Arrays.toString(values())));
    }
  }

  private final ImmutableMap<String, String> attributes;
  @Nullable private final String value;
  private final Type valueType;

  public static XmlResourceValue createWithValue(Type valueType, String value) {
    return of(valueType, ImmutableMap.<String, String>of(), value);
  }

  public static XmlResourceValue withAttributes(
      Type valueType, ImmutableMap<String, String> attributes) {
    return of(valueType, attributes, null);
  }

  public static XmlResourceValue itemWithFormattedValue(
      ResourceType resourceType, String format, String value) {
    return of(Type.ITEM, ImmutableMap.of("type", resourceType.getName(), "format", format), value);
  }

  public static XmlResourceValue itemWithValue(
      ResourceType resourceType, String value) {
    return of(Type.ITEM, ImmutableMap.of("type", resourceType.getName()), value);
  }

  public static XmlResourceValue itemPlaceHolderFor(ResourceType resourceType) {
    return withAttributes(Type.ITEM, ImmutableMap.of("type", resourceType.getName()));
  }

  public static XmlResourceValue of(
      Type valueType, ImmutableMap<String, String> attributes, @Nullable String value) {
    return new SimpleXmlResourceValue(valueType, attributes, value);
  }

  private SimpleXmlResourceValue(
      Type valueType, ImmutableMap<String, String> attributes, String value) {
    this.valueType = valueType;
    this.value = value;
    this.attributes = attributes;
  }

  @Override
  public void write(
      FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) {

    StartTag startTag =
        mergedDataWriter
            .define(key)
            .derivedFrom(source)
            .startTag(valueType.tagName)
            .named(key)
            .addAttributesFrom(attributes.entrySet());

    if (value != null) {
      startTag.closeTag().addCharactersOf(value).endTag().save();
    } else {
      startTag.closeUnaryTag().save();
    }
  }

  @SuppressWarnings("deprecation")
  public static XmlResourceValue from(SerializeFormat.DataValueXml proto) {
    return of(
        Type.valueOf(proto.getValueType()),
        ImmutableMap.copyOf(proto.getAttribute()),
        proto.hasValue() ? proto.getValue() : null);
  }

  public static XmlResourceValue from(Value proto, ResourceType resourceType) {
    Item item = proto.getItem();
    String stringValue = null;

    if (item.hasStr()) {
      stringValue = XmlEscapers.xmlContentEscaper().escape(item.getStr().getValue());
    } else if (item.hasRef()) {
      stringValue = "@" + item.getRef().getName();
    } else if (item.hasStyledStr()) {
      StyledString styledString = item.getStyledStr();
      StringBuilder stringBuilder = new StringBuilder(styledString.getValue());

      for (Span span : styledString.getSpanList()) {
        stringBuilder.append(
            String.format(";%s,%d,%d", span.getTag(), span.getFirstChar(), span.getLastChar()));
      }
      stringValue = stringBuilder.toString();
    } else if ((resourceType == ResourceType.COLOR
        || resourceType == ResourceType.DRAWABLE) && item.hasPrim()) {
      stringValue =
          String.format("#%1$8s", Integer.toHexString(item.getPrim().getData())).replace(' ', '0');
    } else if (resourceType == ResourceType.INTEGER && item.hasPrim()){
      stringValue = Integer.toString(item.getPrim().getData());
    } else if (resourceType == ResourceType.BOOL && item.hasPrim()) {
      stringValue = item.getPrim().getData() == 0 ? "false" : "true";
    } else if (resourceType == ResourceType.FRACTION
        || resourceType == ResourceType.DIMEN
        || resourceType == ResourceType.STRING) {
      stringValue = Integer.toString(item.getPrim().getData());
    } else {
      throw new IllegalArgumentException(
          String.format("'%s' with value %s is not a simple resource type.", resourceType, proto));
    }

    return of(
        Type.valueOf(resourceType.toString().toUpperCase(Locale.ENGLISH)),
        ImmutableMap.of(),
        stringValue);
  }

  @Override
  public void writeResourceToClass(FullyQualifiedName key, AndroidResourceSymbolSink sink) {
    sink.acceptSimpleResource(key.type(), key.name());
  }

  @Override
  public int serializeTo(int sourceId, Namespaces namespaces, OutputStream output)
      throws IOException {
    SerializeFormat.DataValue.Builder builder =
        XmlResourceValues.newSerializableDataValueBuilder(sourceId);
    DataValueXml.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.
            .putAllAttribute(attributes);
    if (value != null) {
      xmlValueBuilder.setValue(value);
    }
    builder.setXmlValue(xmlValueBuilder.setValueType(valueType.name()));
    return XmlResourceValues.serializeProtoDataValue(output, builder);
  }

  @Override
  public int hashCode() {
    return Objects.hash(valueType, attributes, value);
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof SimpleXmlResourceValue)) {
      return false;
    }
    SimpleXmlResourceValue other = (SimpleXmlResourceValue) obj;
    return Objects.equals(valueType, other.valueType)
        && Objects.equals(attributes, other.attributes)
        && Objects.equals(value, other.value);
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(getClass())
        .add("valueType", valueType)
        .add("attributes", attributes)
        .add("value", value)
        .toString();
  }

  @Override
  public XmlResourceValue combineWith(XmlResourceValue value) {
    throw new IllegalArgumentException(this + " is not a combinable resource.");
  }
  
  @Override
  public String asConflictStringWith(DataSource source) {
    if (value != null) {
      return String.format(" %s (with value %s)", source.asConflictString(), value);
    }
    return source.asConflictString();
  }
}