// Copyright 2017 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.google.common.truth.Truth.assertAbout; import static com.google.common.truth.Truth.assertThat; import com.android.resources.ResourceType; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; import com.google.common.jimfs.Jimfs; import com.google.common.truth.FailureStrategy; import com.google.common.truth.SubjectFactory; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; import com.google.devtools.build.android.xml.ArrayXmlResourceValue; import com.google.devtools.build.android.xml.ArrayXmlResourceValue.ArrayType; import com.google.devtools.build.android.xml.AttrXmlResourceValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.BooleanResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.ColorResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.DimensionResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.EnumResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.FlagResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.FloatResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.FractionResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.IntegerResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.ReferenceResourceXmlAttrValue; import com.google.devtools.build.android.xml.AttrXmlResourceValue.StringResourceXmlAttrValue; 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 java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.xml.stream.XMLStreamException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link DataResourceXml}. */ @RunWith(JUnit4.class) public class DataResourceXmlTest { static final ImmutableMap<String, String> XLIFF_NAMESPACES = ImmutableMap.of("xliff", "urn:oasis:names:tc:xliff:document:1.2"); static final String END_RESOURCES = new String(AndroidDataWriter.END_RESOURCES); private FullyQualifiedName.Factory fqnFactory; private FileSystem fs; @Before public void createCleanEnvironment() { fs = Jimfs.newFileSystem(); fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.<String>of()); } private Path writeResourceXml(String... xml) throws IOException { return writeResourceXml(ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(), xml); } private Path writeResourceXml(Map<String, String> namespaces, Map<String, String> attributes, String... xml) throws IOException { Path values = fs.getPath("root/values/values.xml"); Files.createDirectories(values.getParent()); StringBuilder builder = new StringBuilder(); builder.append(AndroidDataWriter.PRELUDE).append("<resources"); for (Entry<String, String> entry : namespaces.entrySet()) { builder .append(" xmlns:") .append(entry.getKey()) .append("=\"") .append(entry.getValue()) .append("\""); } for (Entry<String, String> entry : attributes.entrySet()) { builder .append(" ") .append(entry.getKey()) .append("=\"") .append(entry.getValue()) .append("\""); } builder.append(">"); Joiner.on("\n").appendTo(builder, xml); builder.append(END_RESOURCES); Files.write(values, builder.toString().getBytes(StandardCharsets.UTF_8)); return values; } private void parseResourcesFrom( Path path, Map<DataKey, DataResource> toOverwrite, Map<DataKey, DataResource> toCombine) throws XMLStreamException, IOException { DataResourceXml.parse( XmlResourceValues.getXmlInputFactory(), path, fqnFactory, new FakeConsumer(toOverwrite), new FakeConsumer(toCombine)); } private FullyQualifiedName fqn(String raw) { return fqnFactory.parse(raw); } @Test public void simpleXmlResources() throws Exception { Path path = writeResourceXml( "<string name='exit' description=\"& egress -> "\">way out</string>", "<bool name='canExit'>false</bool>", "<color name='exitColor'>#FF000000</color>", "<dimen name='exitSize'>20sp</dimen>", "<integer name='exitInt'>20</integer>", "<fraction name='exitFraction'>%20</fraction>", "<drawable name='exitBackground'>#99000000</drawable>", "<item name='some_id' type='id'/>", "<item name='reference_id' type='id'>@id/some_id</item>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("string/exit"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.of( SimpleXmlResourceValue.Type.STRING, ImmutableMap.of("description", "& egress -> ""), "way out")), // Value fqn("bool/canExit"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.BOOL, "false")), // Value fqn("color/exitColor"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.COLOR, "#FF000000")), // Value fqn("dimen/exitSize"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.DIMEN, "20sp")), // Value fqn("integer/exitInt"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.INTEGER, "20")), // Value fqn("fraction/exitFraction"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.FRACTION, "%20")), // Value fqn("drawable/exitBackground"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.DRAWABLE, "#99000000")) // Value ); assertThat(toCombine) .containsExactly( fqn("id/some_id"), // Key DataResourceXml.createWithNoNamespace(path, IdXmlResourceValue.of()), // Value fqn("id/reference_id"), // Key DataResourceXml.createWithNoNamespace( path, IdXmlResourceValue.of("@id/some_id")) // Value ); } @Test public void skipIgnored() throws Exception { Path path = writeResourceXml("<skip/>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite).isEmpty(); assertThat(toCombine).isEmpty(); } @Test public void eatCommentIgnored() throws Exception { Path path = writeResourceXml("<eat-comment/>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite).isEmpty(); assertThat(toCombine).isEmpty(); } @Test public void itemSimpleXmlResources() throws Exception { Path path = writeResourceXml( "<item type='dimen' name='exitSizePercent'>20%</item>", "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>", "<item type='fraction' name='denom'>%5</item>", "<item name='subtype_id' type='id'>0x6f972360</item>", "<item type='array' name='oboes'/>", "<item type='drawable' name='placeholder'/>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("dimen/exitSizePercent"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.itemWithValue(ResourceType.DIMEN, "20%")), // Value fqn("dimen/exitSizeFloat"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.itemWithFormattedValue( ResourceType.DIMEN, "float", "20.0")), // Value fqn("drawable/placeholder"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.itemPlaceHolderFor(ResourceType.DRAWABLE)), // Value fqn("array/oboes"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.itemPlaceHolderFor(ResourceType.ARRAY)), // Value fqn("fraction/denom"), // Key DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.itemWithValue(ResourceType.FRACTION, "%5")) // Value ); } @Test public void styleableXmlResourcesEnum() throws Exception { Path path = writeResourceXml( "<declare-styleable name='Theme'>", " <attr name=\"labelPosition\" format=\"enum\">", " <enum name=\"left\" value=\"0\"/>", " <enum name=\"right\" value=\"1\"/>", " </attr>", "</declare-styleable>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("attr/labelPosition"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( EnumResourceXmlAttrValue.asEntryOf("left", "0", "right", "1"))) // Value ); assertThat(toCombine) .containsExactly( fqn("styleable/Theme"), // Key DataResourceXml.createWithNoNamespace( path, StyleableXmlResourceValue.createAllAttrAsDefinitions( fqnFactory.parse("attr/labelPosition"))) // Value ); } @Test public void styleableXmlResourcesString() throws Exception { Path path = writeResourceXml( "<declare-styleable name='UnusedStyleable'>", " <attr name='attribute1'/>", " <attr name='attribute2'/>", "</declare-styleable>", "<attr name='attribute1' format='string'/>", "<attr name='attribute2' format='string'/>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("attr/attribute1"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( StringResourceXmlAttrValue.asEntry())), // Value fqn("attr/attribute2"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( StringResourceXmlAttrValue.asEntry())) // Value ); assertThat(toCombine) .containsExactly( fqn("styleable/UnusedStyleable"), // Key DataResourceXml.createWithNoNamespace( path, StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/attribute1"), fqnFactory.parse("attr/attribute2"))) // Value ); } @Test public void pluralXmlResources() throws Exception { Path path = writeResourceXml( "<plurals name='numberOfSongsAvailable'>", " <item quantity='one'>%d song found.</item>", " <item quantity='other'>%d songs found.</item>", "</plurals>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("plurals/numberOfSongsAvailable"), // Key DataResourceXml.createWithNoNamespace( path, PluralXmlResourceValue.createWithoutAttributes( ImmutableMap.of( "one", "%d song found.", "other", "%d songs found."))) // Value ); } @Test public void pluralXmlResourcesWithTrailingCharacters() throws Exception { Path path = writeResourceXml( "<plurals name='numberOfSongsAvailable'>", " <item quantity='one'>%d song found.</item> // this is an invalid comment.", " <item quantity='other'>%d songs found.</item>typo", "</plurals>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("plurals/numberOfSongsAvailable"), // Key DataResourceXml.createWithNoNamespace( path, PluralXmlResourceValue.createWithoutAttributes( ImmutableMap.of( "one", "%d song found.", "other", "%d songs found."))) // Value ); } @Test public void styleResources() throws Exception { Path path = writeResourceXml( "<style name=\"candlestick_maker\" parent=\"@style/wax_maker\">\n" + " <item name=\"texture\">waxy</item>\n" + "</style>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("style/candlestick_maker"), // Key DataResourceXml.createWithNoNamespace( path, StyleXmlResourceValue.of( "@style/wax_maker", ImmutableMap.of("texture", "waxy"))) // Value ); } @Test public void styleResourcesNoParent() throws Exception { Path path = writeResourceXml( "<style name=\"CandlestickMaker\">\n" + " <item name=\"texture\">waxy</item>\n" + "</style>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("style/CandlestickMaker"), // Key DataResourceXml.createWithNoNamespace( path, StyleXmlResourceValue.of(null, ImmutableMap.of("texture", "waxy"))) // Value ); } @Test public void styleResourcesForceNoParent() throws Exception { Path path = writeResourceXml( // using a '.' implies a parent Candlestick, parent='' corrects to no parent. "<style name=\"Candlestick.Maker\" parent=\"\">\n" + " <item name=\"texture\">waxy</item>\n" + "</style>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("style/Candlestick.Maker"), // Key DataResourceXml.createWithNoNamespace( path, StyleXmlResourceValue.of("", ImmutableMap.of("texture", "waxy"))) // Value ); } @Test public void styleResourcesLazyReference() throws Exception { Path path = writeResourceXml( "<style name=\"candlestick_maker\" parent=\"AppTheme\">\n" + " <item name=\"texture\">waxy</item>\n" + "</style>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("style/candlestick_maker"), // Key DataResourceXml.createWithNoNamespace( path, StyleXmlResourceValue.of("AppTheme", ImmutableMap.of("texture", "waxy"))) // Value ); } @Test public void arrayXmlResources() throws Exception { Path path = writeResourceXml( "<array name='icons'>", " <item>@drawable/home</item>", " <item>@drawable/settings</item>", " <item>@drawable/logout</item>", "</array>", "<array name='colors'>", " <item>#FFFF0000</item>", " <item>#FF00FF00</item>", " <item>#FF0000FF</item>", "</array>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("array/icons"), // Key DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of( ArrayType.ARRAY, "@drawable/home", "@drawable/settings", "@drawable/logout")), // Value fqn("array/colors"), // Key DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of( ArrayType.ARRAY, "#FFFF0000", "#FF00FF00", "#FF0000FF")) // Value ); } @Test public void stringArrayXmlResources() throws Exception { Path path = writeResourceXml( "<string-array name='characters'>", " <item>boojum</item>", " <item>snark</item>", " <item>bellman</item>", " <item>barrister</item>", " <item>\\\"billiard-marker\\\"</item>", "</string-array>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("array/characters"), DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of( ArrayType.STRING_ARRAY, "boojum", "snark", "bellman", "barrister", "\\\"billiard-marker\\\""))); assertThat(toCombine).isEmpty(); } @Test public void integerArrayXmlResources() throws Exception { Path path = writeResourceXml( "<integer-array name='bits'>", " <item>4</item>", " <item>8</item>", " <item>16</item>", " <item>32</item>", "</integer-array>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("array/bits"), // Key DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of(ArrayType.INTEGER_ARRAY, "4", "8", "16", "32")) // Value ); } @Test public void attrFlagXmlResources() throws Exception { Path path = writeResourceXml( " <attr name=\"labelPosition\">", " <flag name=\"left\" value=\"0\"/>", " <flag name=\"right\" value=\"1\"/>", " </attr>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("attr/labelPosition"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( FlagResourceXmlAttrValue.asEntryOf( "left", "0", "right", "1"))) // Value ); } @Test public void attrMultiFormatImplicitFlagXmlResources() throws Exception { Path path = writeResourceXml( " <attr name='labelPosition' format='reference'>", " <flag name='left' value='0'/>", " <flag name='right' value='1'/>", " </attr>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("attr/labelPosition"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( ReferenceResourceXmlAttrValue.asEntry(), FlagResourceXmlAttrValue.asEntryOf( "left", "0", "right", "1"))) // Value ); } @Test public void attrMultiFormatResources() throws Exception { Path path = writeResourceXml( "<attr name='labelPosition' ", "format='color|boolean|dimension|float|integer|string|fraction|reference' />"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("attr/labelPosition"), // Key DataResourceXml.createWithNoNamespace( path, AttrXmlResourceValue.fromFormatEntries( ColorResourceXmlAttrValue.asEntry(), BooleanResourceXmlAttrValue.asEntry(), ReferenceResourceXmlAttrValue.asEntry(), DimensionResourceXmlAttrValue.asEntry(), FloatResourceXmlAttrValue.asEntry(), IntegerResourceXmlAttrValue.asEntry(), StringResourceXmlAttrValue.asEntry(), FractionResourceXmlAttrValue.asEntry())) // Value ); } @Test public void publicXmlResource() throws Exception { Path path = writeResourceXml( "<string name='exit'>way out</string>", "<public type='string' name='exit' id='0x123'/>"); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite) .containsExactly( fqn("string/exit"), DataResourceXml.createWithNoNamespace( path, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "way out"))); assertThat(toCombine) .containsExactly( fqn("public/exit"), DataResourceXml.createWithNoNamespace( path, PublicXmlResourceValue.create(ResourceType.STRING, Optional.of(0x123)))); } @Test public void resourcesAttribute() throws Exception { Namespaces namespaces = Namespaces.from( ImmutableMap.of("tools", "http://schemas.android.com/tools")); Path path = writeResourceXml( namespaces.asMap(), ImmutableMap.of("tools:foo", "fooVal", "tools:ignore", "ignoreVal")); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); FullyQualifiedName fooFqn = fqn("<resources>/{http://schemas.android.com/tools}foo"); assertThat(toOverwrite) .containsExactly( fooFqn, DataResourceXml.createWithNamespaces( path, ResourcesAttribute.of(fooFqn, "tools:foo", "fooVal"), namespaces) ); FullyQualifiedName ignoreFqn = fqn("<resources>/{http://schemas.android.com/tools}ignore"); assertThat(toCombine) .containsExactly( ignoreFqn, DataResourceXml.createWithNamespaces( path, ResourcesAttribute.of(ignoreFqn, "tools:ignore", "ignoreVal"), namespaces) ); } @Test public void writeSimpleXmlResources() throws Exception { Path source = writeResourceXml( "<string name='exit'>way <a href=\"out.html\">out</a></string>", "<bool name='canExit'>false</bool>", "<color name='exitColor'>#FF000000</color>", "<integer name='exitInt'>5</integer>", "<drawable name='reference'/>"); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/exit"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "<string name='exit'>way <a href=\"out.html\">out</a></string>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("bool/canExit"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<bool name='canExit'>false</bool>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("color/exitColor"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "<color name='exitColor'>#FF000000</color>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("integer/exitInt"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<integer name='exitInt'>5</integer>")); } @Test public void writeItemResources() throws Exception { Path source = writeResourceXml( "<item type='dimen' name='exitSizePercent'>20%</item>", "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>", "<item name='exitId' type='id'/>", "<item name='frac' type='fraction'>5%</item>"); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("dimen/exitSizePercent"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "<item type='dimen' name='exitSizePercent'>20%</item>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("dimen/exitSizeFloat"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("fraction/frac"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "<item name='frac' type='fraction'>5%</item>")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("id/exitId"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<item name='exitId' type='id'/>")); } @Test public void writeStringResourceWithXliffNamespace() throws Exception { Path source = fs.getPath("root/values/values.xml"); Files.createDirectories(source.getParent()); Files.write( source, ("<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">" + "<string name=\"star_rating\">Check out our 5\n" + " <xliff:g id=\"star\">\\u2605</xliff:g>\n" + "</string>" + "</resources>") .getBytes(StandardCharsets.UTF_8)); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/star_rating"))) .xmlContentsIsEqualTo( resourcesXmlFrom( XLIFF_NAMESPACES, ImmutableMap.<String, String>of(), source, "<string name=\"star_rating\">Check out our 5", " <xliff:g id=\"star\">\\u2605</xliff:g>", "</string>")); } @Test public void writeStringResourceCData() throws Exception { String[] xml = { "<string name='cdata'><![CDATA[<b>Jabber, Jabber</b><br><br>\n Wock!]]></string>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/cdata"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStringResourceWithNamespace() throws Exception { Path source = fs.getPath("root/values/values.xml"); Files.createDirectories(source.getParent()); Files.write( source, ("<resources xmlns:ns1=\"urn:oasis:names:tc:xliff:document:1.2\">" + "<string name=\"star_rating\">Check out our 5\n" + " <ns1:g xmlns:foglebert=\"defogle\" foglebert:id=\"star\">\\u2605</ns1:g>\n" + "</string>" + "</resources>") .getBytes(StandardCharsets.UTF_8)); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/star_rating"))) .xmlContentsIsEqualTo( resourcesXmlFrom( ImmutableMap.of("ns1", "urn:oasis:names:tc:xliff:document:1.2"), ImmutableMap.<String, String>of(), source, "<string name=\"star_rating\">Check out our 5 ", "<ns1:g xmlns:foglebert=\"defogle\" " + "foglebert:id=\"star\">\\u2605</ns1:g>", "</string>")); } @Test public void writeStringResourceWithUnusedNamespace() throws Exception { Path source = fs.getPath("root/values/values.xml"); Files.createDirectories(source.getParent()); Files.write( source, ("<resources xmlns:ns1=\"urn:oasis:names:tc:xliff:document:1.2\">" + "<string name=\"star_rating\">" + "not yet implemented\n" + "</string>" + "</resources>") .getBytes(StandardCharsets.UTF_8)); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/star_rating"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "<string name=\"star_rating\">", "not yet implemented\n", "</string>")); } @Test public void writeStringResourceWithEscapedValues() throws Exception { String[] xml = { "<string name=\"AAP_SUGGEST_ACCEPT_SUGGESTION\">", " <b><xliff:g id=\"name\" example=\"Pizza hut\">%1$s</xliff:g></b> " + "already exists at <b><xliff:g id=\"address\" " + "example=\"123 main street\">%2$s</xliff:g></b><br>", " <br>", " Is this the place you\\'re trying to add?\n", "</string>" }; Path source = writeResourceXml(XLIFF_NAMESPACES, ImmutableMap.<String, String>of(), xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/AAP_SUGGEST_ACCEPT_SUGGESTION"))) .xmlContentsIsEqualTo( resourcesXmlFrom(XLIFF_NAMESPACES, ImmutableMap.<String, String>of(), source, xml)); } @Test public void writeStyleableXmlResource() throws Exception { String[] xml = { "<declare-styleable name='Theme'>", " <attr name=\"labelPosition\" />", "</declare-styleable>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("styleable/Theme"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStyleableXmlResourceReference() throws Exception { String[] xml = { "<declare-styleable name='Theme'>", " <attr name=\"labelColor\" format=\"color\" />", "</declare-styleable>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("styleable/Theme"), fqn("attr/labelColor"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writePluralXmlResources() throws Exception { String[] xml = { "<plurals name='numberOfSongsAvailable' format='none'>", " <item quantity='one'>%d song found.</item>", " <item quantity='other'>%d songs found.</item>", "</plurals>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("plurals/numberOfSongsAvailable"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writePublicXmlResources() throws Exception { String[] xml = { "<public name='bar' type='dimen' id='0x7f030003' />", "<public name='foo' type='dimen' />", }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("public/bar"), fqn("public/foo"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeArrayXmlResources() throws Exception { String[] xml = { "<array name='icons'>", " <item>@drawable/home</item>", " <item>@drawable/settings</item>", " <item>@drawable/logout</item>", "</array>", }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/icons"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStringArrayXmlResources() throws Exception { String[] xml = { "<string-array name='rosebud' translatable='false'>", " <item>Howard Hughes</item>", " <item>Randolph Hurst</item>", "</string-array>", }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/rosebud"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeIntegerArrayXmlResources() throws Exception { String[] xml = { "<integer-array name='bits'>", " <item>4</item>", " <item>8</item>", " <item>16</item>", " <item>32</item>", "</integer-array>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/bits"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeAttrFlagXmlResources() throws Exception { String[] xml = { " <attr name=\"labelPosition\">", " <flag name=\"left\" value=\"0\"/>", " <flag name=\"right\" value=\"1\"/>", " </attr>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, " <attr name=\"labelPosition\">", " <flag name=\"left\" value=\"0\"/>", " <flag name=\"right\" value=\"1\"/>", " </attr>")); } @Test public void writeAttrMultiFormatImplicitFlagXmlResources() throws Exception { String[] xml = { " <attr name='labelPosition' format='reference'>", " <flag name='left' value='0'/>", " <flag name='right' value='1'/>", " </attr>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, " <attr name='labelPosition' format='reference'>", " <flag name='left' value='0'/>", " <flag name='right' value='1'/>", " </attr>")); } @Test public void writeAttrMultiFormatResources() throws Exception { String[] xml = { "<attr name='labelPosition' ", "format='boolean|color|dimension|float|fraction|integer|reference|string'/>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStyle() throws Exception { String[] xml = { "<style name='candlestick_maker' parent='@style/wax_maker'>\n" + " <item name='texture'>waxy</item>\n" + "</style>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("style/candlestick_maker"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeForceNoParentStyle() throws Exception { String[] xml = { "<style name='candlestick_maker' parent=''>\n", " <item name='texture'>waxy</item>\n", "</style>" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("style/candlestick_maker"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeResourceAttributes() throws Exception { Path source = writeResourceXml( ImmutableMap.of("tools", "http://schemas.android.com/tools"), ImmutableMap.of("tools:foo", "fooVal")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("<resources>/{http://schemas.android.com/tools}foo"))) .xmlContentsIsEqualTo( resourcesXmlFrom( ImmutableMap.of("tools", "http://schemas.android.com/tools"), ImmutableMap.of("tools:foo", "fooVal"), null)); } @Test public void serializeMultipleSimpleXmlResources() throws Exception { Path serialized = fs.getPath("out/out.bin"); Path source = fs.getPath("res/values/values.xml"); FullyQualifiedName stringKey = fqn("string/exit"); DataResourceXml stringValue = DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.STRING, "way out")); FullyQualifiedName nullStringKey = fqn("string/nullexit"); DataResourceXml nullStringValue = DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.STRING, null)); FullyQualifiedName boolKey = fqn("bool/canExit"); DataResourceXml boolValue = DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.BOOL, "false")); FullyQualifiedName colorKey = fqn("color/exitColor"); DataResourceXml colorValue = DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.COLOR, "#FF000000")); FullyQualifiedName dimenKey = fqn("dimen/exitSize"); DataResourceXml dimenValue = DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.DIMEN, "20sp")); AndroidDataSerializer serializer = AndroidDataSerializer.create(); serializer.queueForSerialization(stringKey, stringValue); serializer.queueForSerialization(nullStringKey, nullStringValue); serializer.queueForSerialization(boolKey, boolValue); serializer.queueForSerialization(colorKey, colorValue); serializer.queueForSerialization(dimenKey, dimenValue); serializer.flushTo(serialized); AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( serialized, KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null)); assertThat(toOverwrite) .containsExactly( stringKey, stringValue, boolKey, boolValue, colorKey, colorValue, nullStringKey, nullStringValue, dimenKey, dimenValue); assertThat(toCombine).isEmpty(); } @Test public void serializeItemXmlResources() throws Exception { Path source = fs.getPath("res/values/values.xml"); assertSerialization( fqn("dimen/exitSizePercent"), DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.itemWithValue(ResourceType.DIMEN, "20%"))); assertSerialization( fqn("dimen/exitSizeFloat"), DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.itemWithFormattedValue(ResourceType.DIMEN, "float", "20.0"))); assertSerialization( fqn("fraction/denom"), DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.COLOR, "5%"))); assertSerialization( fqn("id/subtype_afrikaans"), DataResourceXml.createWithNoNamespace( source, SimpleXmlResourceValue.itemWithValue(ResourceType.ID, "0x6f972360"))); } @Test public void serializeStyleableXmlResource() throws Exception { Path serialized = fs.getPath("out/out.bin"); Path source = fs.getPath("res/values/values.xml"); FullyQualifiedName attrKey = fqn("attr/labelPosition"); DataResourceXml attrValue = DataResourceXml.createWithNoNamespace( source, AttrXmlResourceValue.fromFormatEntries( EnumResourceXmlAttrValue.asEntryOf("left", "0", "right", "1"))); FullyQualifiedName themeKey = fqn("styleable/Theme"); DataResourceXml themeValue = DataResourceXml.createWithNoNamespace( source, StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/labelPosition"))); AndroidDataSerializer serializer = AndroidDataSerializer.create(); serializer.queueForSerialization(attrKey, attrValue); serializer.queueForSerialization(themeKey, themeValue); serializer.flushTo(serialized); AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( serialized, KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null)); assertThat(toOverwrite).containsEntry(attrKey, attrValue); assertThat(toCombine).containsEntry(themeKey, themeValue); } @Test public void serializePlurals() throws Exception { Path path = fs.getPath("res/values/values.xml"); assertSerialization( fqn("plurals/numberOfSongsAvailable"), DataResourceXml.createWithNoNamespace( path, PluralXmlResourceValue.createWithoutAttributes( ImmutableMap.of( "one", "%d song found.", "other", "%d songs found.")))); } @Test public void serializeArrays() throws Exception { Path path = fs.getPath("res/values/values.xml"); assertSerialization( fqn("plurals/numberOfSongsAvailable"), DataResourceXml.createWithNoNamespace( path, PluralXmlResourceValue.createWithoutAttributes( ImmutableMap.of( "one", "%d song found.", "other", "%d songs found.")))); assertSerialization( fqn("array/icons"), DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of( ArrayType.ARRAY, "@drawable/home", "@drawable/settings", "@drawable/logout"))); assertSerialization( fqn("array/colors"), DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of(ArrayType.ARRAY, "#FFFF0000", "#FF00FF00", "#FF0000FF"))); assertSerialization( fqn("array/characters"), DataResourceXml.createWithNoNamespace( path, ArrayXmlResourceValue.of( ArrayType.STRING_ARRAY, "boojum", "snark", "bellman", "barrister", "\\\"billiard-marker\\\""))); } @Test public void serializeAttrFlag() throws Exception { assertSerialization( fqn("attr/labelPosition"), DataResourceXml.createWithNoNamespace( fs.getPath("res/values/values.xml"), AttrXmlResourceValue.fromFormatEntries( FlagResourceXmlAttrValue.asEntryOf( "left", "0", "right", "1")))); } @Test public void serializeId() throws Exception { assertSerialization( fqn("id/squark"), DataResourceXml.createWithNoNamespace( fs.getPath("res/values/values.xml"), IdXmlResourceValue.of())); } @Test public void serializePublic() throws Exception { assertSerialization( fqn("public/park"), DataResourceXml.createWithNoNamespace( fs.getPath("res/values/public.xml"), PublicXmlResourceValue.of( ImmutableMap.of( ResourceType.DIMEN, Optional.of(0x7f040000), ResourceType.STRING, Optional.of(0x7f050000)) ))); } @Test public void serializeStyle() throws Exception { assertSerialization( fqn("style/snark"), DataResourceXml.createWithNoNamespace( fs.getPath("res/values/styles.xml"), StyleXmlResourceValue.of(null, ImmutableMap.of("look", "boojum")))); } @Test public void assertMultiFormatAttr() throws Exception { assertSerialization( fqn("attr/labelPosition"), DataResourceXml.createWithNoNamespace( fs.getPath("res/values/values.xml"), AttrXmlResourceValue.fromFormatEntries( ColorResourceXmlAttrValue.asEntry(), BooleanResourceXmlAttrValue.asEntry(), ReferenceResourceXmlAttrValue.asEntry(), DimensionResourceXmlAttrValue.asEntry(), FloatResourceXmlAttrValue.asEntry(), IntegerResourceXmlAttrValue.asEntry(), StringResourceXmlAttrValue.asEntry(), FractionResourceXmlAttrValue.asEntry()))); } private void assertSerialization(FullyQualifiedName key, DataValue value) throws Exception { Path serialized = fs.getPath("out/out.bin"); Files.deleteIfExists(serialized); Path manifestPath = fs.getPath("AndroidManifest.xml"); Files.deleteIfExists(manifestPath); Files.createFile(manifestPath); AndroidDataSerializer serializer = AndroidDataSerializer.create(); serializer.queueForSerialization(key, value); serializer.flushTo(serialized); AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); deserializer.read( serialized, KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null)); if (key.isOverwritable()) { assertThat(toOverwrite).containsEntry(key, value); assertThat(toCombine).isEmpty(); } else { assertThat(toCombine).containsEntry(key, value); assertThat(toOverwrite).isEmpty(); } } private String[] resourcesXmlFrom(Path source, String... lines) { return resourcesXmlFrom(ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(), source, lines); } private String[] resourcesXmlFrom(Map<String, String> namespaces, Map<String, String> attributes, Path source, String... lines) { FluentIterable<String> xml = FluentIterable.of(new String(AndroidDataWriter.PRELUDE)) .append("<resources") .append( FluentIterable.from(namespaces.entrySet()) .transform( new Function<Entry<String, String>, String>() { @Override public String apply(Entry<String, String> input) { return String.format(" xmlns:%s=\"%s\"", input.getKey(), input.getValue()); } }) .join(Joiner.on(""))) .append( FluentIterable.from(attributes.entrySet()) .transform( new Function<Entry<String, String>, String>() { @Override public String apply(Entry<String, String> input) { return String.format(" %s=\"%s\"", input.getKey(), input.getValue()); } }) .join(Joiner.on(""))); if (source == null && (lines == null || lines.length == 0)) { xml = xml.append("/>"); } else { xml = xml.append(">"); if (source != null) { xml = xml.append(String.format("<!-- %s --> <eat-comment/>", source)); } if (lines != null) { xml = xml.append(lines); } xml = xml.append(END_RESOURCES); } return xml.toArray(String.class); } private Path parsedAndWritten(Path path, FullyQualifiedName... fqns) throws XMLStreamException, IOException, MergingException { final Map<DataKey, DataResource> toOverwrite = new HashMap<>(); final Map<DataKey, DataResource> toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); Path out = fs.getPath("out"); if (Files.exists(out)) { MoreFiles.deleteRecursively(out, RecursiveDeleteOption.ALLOW_INSECURE); } // find and write the resource -- the categorization is tested during parsing. AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(out); for (FullyQualifiedName fqn : fqns) { if (toOverwrite.containsKey(fqn)) { toOverwrite.get(fqn).writeResource(fqn, mergedDataWriter); } else if (toCombine.containsKey(fqn)) { toCombine.get(fqn).writeResource(fqn, mergedDataWriter); } } mergedDataWriter.flush(); return mergedDataWriter.resourceDirectory().resolve("values/values.xml"); } private static final SubjectFactory<PathsSubject, Path> resourcePaths = new SubjectFactory<PathsSubject, Path>() { @Override public PathsSubject getSubject(FailureStrategy failureStrategy, Path path) { return new PathsSubject(failureStrategy, path); } }; private static class FakeConsumer implements ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> { private final Map<DataKey, DataResource> target; FakeConsumer(Map<DataKey, DataResource> target) { this.target = target; } @Override public void consume(DataKey key, DataResource value) { target.put(key, value); } } }