// 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.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.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; 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 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.of()); } private Path writeResourceXml(String... xml) throws IOException { return writeResourceXml(ImmutableMap.of(), ImmutableMap.of(), xml); } private Path writeResourceXml(Map namespaces, Map 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(" entry : namespaces.entrySet()) { builder .append(" xmlns:") .append(entry.getKey()) .append("=\"") .append(entry.getValue()) .append("\""); } for (Entry 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 toOverwrite, Map 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( "way out", "false", "#FF000000", "20sp", "20", "%20", "#99000000", "", "@id/some_id"); final Map toOverwrite = new HashMap<>(); final Map 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(""); final Map toOverwrite = new HashMap<>(); final Map toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite).isEmpty(); assertThat(toCombine).isEmpty(); } @Test public void eatCommentIgnored() throws Exception { Path path = writeResourceXml(""); final Map toOverwrite = new HashMap<>(); final Map toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); assertThat(toOverwrite).isEmpty(); assertThat(toCombine).isEmpty(); } @Test public void itemSimpleXmlResources() throws Exception { Path path = writeResourceXml( "20%", "20.0", "%5", "0x6f972360", "", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " ", " ", " ", " ", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " ", " ", "", "", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " %d song found.", " %d songs found.", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " %d song found. // this is an invalid comment.", " %d songs found.typo", ""); final Map toOverwrite = new HashMap<>(); final Map 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( ""); final Map toOverwrite = new HashMap<>(); final Map 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( ""); final Map toOverwrite = new HashMap<>(); final Map 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. ""); final Map toOverwrite = new HashMap<>(); final Map 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( ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " @drawable/home", " @drawable/settings", " @drawable/logout", "", "", " #FFFF0000", " #FF00FF00", " #FF0000FF", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " boojum", " snark", " bellman", " barrister", " \\\"billiard-marker\\\"", ""); final Map toOverwrite = new HashMap<>(); final Map 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( "", " 4", " 8", " 16", " 32", ""); final Map toOverwrite = new HashMap<>(); final Map 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( " ", " ", " ", " "); final Map toOverwrite = new HashMap<>(); final Map 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( " ", " ", " ", " "); final Map toOverwrite = new HashMap<>(); final Map 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( ""); final Map toOverwrite = new HashMap<>(); final Map 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( "way out", ""); final Map toOverwrite = new HashMap<>(); final Map 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 toOverwrite = new HashMap<>(); final Map toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); FullyQualifiedName fooFqn = fqn("/{http://schemas.android.com/tools}foo"); assertThat(toOverwrite) .containsExactly( fooFqn, DataResourceXml.createWithNamespaces( path, ResourcesAttribute.of(fooFqn, "tools:foo", "fooVal"), namespaces) ); FullyQualifiedName ignoreFqn = fqn("/{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( "way out", "false", "#FF000000", "5", ""); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/exit"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "way out")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("bool/canExit"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "false")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("color/exitColor"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "#FF000000")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("integer/exitInt"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "5")); } @Test public void writeItemResources() throws Exception { Path source = writeResourceXml( "20%", "20.0", "", "5%"); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("dimen/exitSizePercent"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "20%")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("dimen/exitSizeFloat"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "20.0")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("fraction/frac"))) .xmlContentsIsEqualTo( resourcesXmlFrom(source, "5%")); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("id/exitId"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, "")); } @Test public void writeStringResourceWithXliffNamespace() throws Exception { Path source = fs.getPath("root/values/values.xml"); Files.createDirectories(source.getParent()); Files.write( source, ("" + "Check out our 5\n" + " \\u2605\n" + "" + "") .getBytes(StandardCharsets.UTF_8)); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/star_rating"))) .xmlContentsIsEqualTo( resourcesXmlFrom( XLIFF_NAMESPACES, ImmutableMap.of(), source, "Check out our 5", " \\u2605", "")); } @Test public void writeStringResourceCData() throws Exception { String[] xml = { "Jabber, Jabber

\n Wock!]]>
" }; 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, ("" + "Check out our 5\n" + " \\u2605\n" + "" + "") .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.of(), source, "Check out our 5 ", "\\u2605", "")); } @Test public void writeStringResourceWithUnusedNamespace() throws Exception { Path source = fs.getPath("root/values/values.xml"); Files.createDirectories(source.getParent()); Files.write( source, ("" + "" + "not yet implemented\n" + "" + "") .getBytes(StandardCharsets.UTF_8)); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/star_rating"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, "", "not yet implemented\n", "")); } @Test public void writeStringResourceWithEscapedValues() throws Exception { String[] xml = { "", " <b>%1$s</b> " + "already exists at <b>%2$s</b><br>", " <br>", " Is this the place you\\'re trying to add?\n", "" }; Path source = writeResourceXml(XLIFF_NAMESPACES, ImmutableMap.of(), xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("string/AAP_SUGGEST_ACCEPT_SUGGESTION"))) .xmlContentsIsEqualTo( resourcesXmlFrom(XLIFF_NAMESPACES, ImmutableMap.of(), source, xml)); } @Test public void writeStyleableXmlResource() throws Exception { String[] xml = { "", " ", "" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("styleable/Theme"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStyleableXmlResourceReference() throws Exception { String[] xml = { "", " ", "" }; 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 = { "", " %d song found.", " %d songs found.", "" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("plurals/numberOfSongsAvailable"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writePublicXmlResources() throws Exception { String[] xml = { "", "", }; 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 = { "", " @drawable/home", " @drawable/settings", " @drawable/logout", "", }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/icons"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStringArrayXmlResources() throws Exception { String[] xml = { "", " Howard Hughes", " Randolph Hurst", "", }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/rosebud"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeIntegerArrayXmlResources() throws Exception { String[] xml = { "", " 4", " 8", " 16", " 32", "" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("array/bits"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeAttrFlagXmlResources() throws Exception { String[] xml = { " ", " ", " ", " " }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, " ", " ", " ", " ")); } @Test public void writeAttrMultiFormatImplicitFlagXmlResources() throws Exception { String[] xml = { " ", " ", " ", " " }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo( resourcesXmlFrom( source, " ", " ", " ", " ")); } @Test public void writeAttrMultiFormatResources() throws Exception { String[] xml = { "" }; Path source = writeResourceXml(xml); assertAbout(resourcePaths) .that(parsedAndWritten(source, fqn("attr/labelPosition"))) .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml)); } @Test public void writeStyle() throws Exception { String[] xml = { "" }; 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 = { "" }; 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("/{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 toOverwrite = new HashMap<>(); final Map 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 toOverwrite = new HashMap<>(); final Map 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 toOverwrite = new HashMap<>(); final Map 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.of(), ImmutableMap.of(), source, lines); } private String[] resourcesXmlFrom(Map namespaces, Map attributes, Path source, String... lines) { FluentIterable xml = FluentIterable.of(new String(AndroidDataWriter.PRELUDE)) .append(", String>() { @Override public String apply(Entry input) { return String.format(" xmlns:%s=\"%s\"", input.getKey(), input.getValue()); } }) .join(Joiner.on(""))) .append( FluentIterable.from(attributes.entrySet()) .transform( new Function, String>() { @Override public String apply(Entry 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(" ", 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 toOverwrite = new HashMap<>(); final Map toCombine = new HashMap<>(); parseResourcesFrom(path, toOverwrite, toCombine); Path out = fs.getPath("out"); if (Files.exists(out)) { Files.walkFileTree( out, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException { if (e != null) { throw e; } Files.delete(directory); return FileVisitResult.CONTINUE; } }); } // 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 resourcePaths = new SubjectFactory() { @Override public PathsSubject getSubject(FailureStrategy failureStrategy, Path path) { return new PathsSubject(failureStrategy, path); } }; private static class FakeConsumer implements ParsedAndroidData.KeyValueConsumer { private final Map target; FakeConsumer(Map target) { this.target = target; } @Override public void consume(DataKey key, DataResource value) { target.put(key, value); } } }