aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2016-06-29 15:49:52 +0000
committerGravatar Dmitry Lomov <dslomov@google.com>2016-06-30 11:40:54 +0000
commitaf2ea37ceab2300a6d40afe65c1e333dfbaa0a11 (patch)
treeebecae8914404203e3944f03f9b6a5b31bb11c3f /src
parent368cc56fb2baa3e21be4acdd2410a4ce8245de93 (diff)
Creates a fluent api that moves the xml generation into the data writer.
The new "define" method replaces the now deprecated writeToValuesXml method. This provides three benefits: * An agnostic writing interface to the XmlResourceValue classes, easing the replacement of them with a proper xml writer. * A delayed write which allows the StyleableXmlResourceValue to claim AttrXmlResourceValue definitions before writing. * Centralized method of source attribution, enabling a less verbose way to indicate multiple values came from a single xml source file. An example of the new interface writing the StyleXmlResourceValue: ValuesResourceDefinition definition = mergedDataWriter .define(key) .derivedFrom(source) .startTag("style") .named(key) .optional() .attribute("parent") .setTo(parent) .closeTag(); for (Entry<String, String> entry : values.entrySet()) { definition = definition .startItemTag() .named(entry.getKey()) .close() .addCharactersOf(entry.getValue()) .endTag(); } definition.endTag().save(); -- MOS_MIGRATED_REVID=126196028
Diffstat (limited to 'src')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java434
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java116
2 files changed, 531 insertions, 19 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java
index 3c857f4a78..9193649d31 100644
--- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java
@@ -14,6 +14,11 @@
package com.google.devtools.build.android;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@@ -21,33 +26,60 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
-import com.android.annotations.Nullable;
import com.android.ide.common.internal.LoggedErrorException;
import com.android.ide.common.internal.PngCruncher;
import com.android.ide.common.res2.MergingException;
import java.io.BufferedWriter;
import java.io.File;
-import java.io.Flushable;
import java.io.IOException;
+import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
-/**
- * Writer for UnwrittenMergedAndroidData.
- */
-public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+/** Writer for UnwrittenMergedAndroidData. */
+public class AndroidDataWriter implements AndroidDataWritingVisitor {
+
+ private static final class CrunchTask implements Callable<Boolean> {
+ private final Path destinationPath;
+ private final Path source;
+ private final PngCruncher cruncher;
+
+ private CrunchTask(PngCruncher cruncher, Path destinationPath, Path source) {
+ this.cruncher = cruncher;
+ this.destinationPath = destinationPath;
+ this.source = source;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ try {
+ Files.createDirectories(destinationPath.getParent());
+ cruncher.crunchPng(source.toFile(), destinationPath.toFile());
+ } catch (InterruptedException | LoggedErrorException e) {
+ throw new MergingException(e);
+ }
+ return Boolean.TRUE;
+ }
+ }
private static final class WriteValuesXmlTask implements Callable<Boolean> {
@@ -83,7 +115,7 @@ public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
}
}
- private final class CopyTask implements Callable<Boolean> {
+ private static final class CopyTask implements Callable<Boolean> {
private final Path sourcePath;
@@ -119,6 +151,8 @@ public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
private final Path destination;
private final Map<String, Map<FullyQualifiedName, Iterable<String>>> valueFragments =
new HashMap<>();
+
+ private final Map<String, ResourceValuesDefinitions> valueTags = new HashMap<>();
private final Path resourceDirectory;
private final Path assetDirectory;
private final PngCruncher cruncher;
@@ -205,17 +239,12 @@ public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
}
@Override
- public void copyResource(Path source, String relativeDestinationPath)
+ public void copyResource(final Path source, final String relativeDestinationPath)
throws IOException, MergingException {
- Path destinationPath = resourceDirectory.resolve(relativeDestinationPath);
+ final Path destinationPath = resourceDirectory.resolve(relativeDestinationPath);
if (!source.getParent().getFileName().toString().startsWith(SdkConstants.FD_RES_RAW)
&& source.getFileName().toString().endsWith(SdkConstants.DOT_PNG)) {
- try {
- Files.createDirectories(destinationPath.getParent());
- cruncher.crunchPng(source.toFile(), destinationPath.toFile());
- } catch (InterruptedException | LoggedErrorException e) {
- throw new MergingException(e);
- }
+ writeTasks.add(executorService.submit(new CrunchTask(cruncher, destinationPath, source)));
} else {
copy(source, destinationPath);
}
@@ -237,6 +266,13 @@ public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
new WriteValuesXmlTask(
resourceDirectory().resolve(entry.getKey()), entry.getValue())));
}
+
+ for (Entry<String, ResourceValuesDefinitions> entry : valueTags.entrySet()) {
+ writeTasks.add(
+ executorService.submit(
+ entry.getValue().createWritingTask(resourceDirectory().resolve(entry.getKey()))));
+ }
+
FailedFutureAggregator.forIOExceptionsWithMessage("Failures during writing.")
.aggregateAndMaybeThrow(writeTasks);
@@ -253,4 +289,372 @@ public class AndroidDataWriter implements Flushable, AndroidDataWritingVisitor {
}
valueFragments.get(valuesPathString).put(key, xmlFragment);
}
+
+ @Override
+ public ValueResourceDefinitionMetadata define(FullyQualifiedName fqn) {
+ String valuesPath = fqn.valuesPath();
+ if (!valueTags.containsKey(valuesPath)) {
+ valueTags.put(valuesPath, new ResourceValuesDefinitions());
+ }
+ return valueTags.get(valuesPath).resource(fqn);
+ }
+
+ /**
+ * A container for the {@linkplain Segment}s of a values.xml file.
+ */
+ private static class ResourceValuesDefinitions {
+ final Multimap<FullyQualifiedName, Segment> segments = ArrayListMultimap.create();
+ final Set<FullyQualifiedName> adopted = new HashSet<>();
+
+ private ValueResourceDefinitionMetadata resource(final FullyQualifiedName fqn) {
+ return new StringValueResourceDefinitionMetadata(segments, adopted, fqn);
+ }
+
+ /** Generates a {@link Callable} that will write the {@link Segment} to the provided path. */
+ public Callable<Boolean> createWritingTask(final Path valuesPath) {
+ return new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ Files.createDirectories(valuesPath.getParent());
+ try (BufferedWriter writer =
+ Files.newBufferedWriter(
+ valuesPath,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE_NEW,
+ StandardOpenOption.WRITE)) {
+ writer.write(START_RESOURCES);
+ Path previousSource = null;
+ for (FullyQualifiedName key :
+ Ordering.natural().immutableSortedCopy(segments.keySet())) {
+ if (!adopted.contains(key)) {
+ for (Segment segment : segments.get(key)) {
+ previousSource = segment.write(previousSource, writer);
+ }
+ }
+ }
+ writer.write(END_RESOURCES);
+ }
+ return Boolean.TRUE;
+ }
+ };
+ }
+ }
+
+ /** Utility function to provide xml namespaced name if a prefix is defined. */
+ private static String maybePrefixName(@Nullable String prefix, String name) {
+ if (prefix == null) {
+ return name;
+ }
+ return prefix + ":" + name;
+ }
+
+ /** Intermediate class that associates a {@link Path} source with an xml definition. */
+ private static class StringValueResourceDefinitionMetadata
+ implements ValueResourceDefinitionMetadata {
+
+ private final Multimap<FullyQualifiedName, Segment> segments;
+ private final Set<FullyQualifiedName> adopted;
+ private final FullyQualifiedName segmentsKey;
+
+ public StringValueResourceDefinitionMetadata(
+ Multimap<FullyQualifiedName, Segment> segments,
+ Set<FullyQualifiedName> adopted,
+ FullyQualifiedName fqn) {
+ this.segments = segments;
+ this.adopted = adopted;
+ this.segmentsKey = fqn;
+ }
+
+ @Override
+ public ValuesResourceDefinition derivedFrom(final Path source) {
+ final SegmentMapper mapper = SegmentMapper.create(segments, adopted, segmentsKey, source);
+ return new StringValuesResourceDefinition(mapper);
+ }
+ }
+
+ /** Intermediate class that builds a string attribute for a {@link StartTag} */
+ private static final class StringAttribute implements Attribute {
+
+ private final String name;
+ private final StringStartTag owner;
+ private final boolean optional;
+
+ public StringAttribute(StringStartTag owner, String name, boolean optional) {
+ this.owner = owner;
+ this.name = name;
+ this.optional = optional;
+ }
+
+ @Override
+ public StartTag setTo(FullyQualifiedName fqn) {
+ return setTo(fqn.name());
+ }
+
+ @Override
+ public StartTag setTo(String value) {
+ if (!(optional && Strings.isNullOrEmpty(value))) {
+ owner.attributes.add(" " + name + "=\"" + value + "\"");
+ }
+ return owner;
+ }
+ }
+
+ /** Intermediate class that collects information for writing an xml start tag string. */
+ private static final class StringStartTag implements StartTag {
+ private final StringValuesResourceDefinition writer;
+ private final String tagName;
+ private final List<String> attributes = new ArrayList<>();
+
+ public StringStartTag(StringValuesResourceDefinition writer, String tagName) {
+ this.writer = writer;
+ this.tagName = tagName;
+ }
+
+ @Override
+ public Attribute attribute(String prefix, String name) {
+ return createAttribute(prefix, name, false);
+ }
+
+ @Override
+ public StringValuesResourceDefinition closeTag() {
+ // Make sure we close this later.
+ writer.tagStack.push(tagName);
+ writer.mapper.add("<" + tagName + Joiner.on("").join(attributes) + ">");
+ return writer;
+ }
+
+ @Override
+ public Attribute attribute(String name) {
+ return attribute(null, name);
+ }
+
+ private Attribute createAttribute(String prefix, String name, boolean optional) {
+ return new StringAttribute(this, maybePrefixName(prefix, name), optional);
+ }
+
+ @Override
+ public Optional optional() {
+ return new Optional() {
+ @Override
+ public Attribute attribute(String prefix, String name) {
+ return createAttribute(prefix, name, true);
+ }
+
+ @Override
+ public Attribute attribute(String name) {
+ return createAttribute(null, name, true);
+ }
+ };
+ }
+
+ @Override
+ public ValuesResourceDefinition closeUnaryTag() {
+ writer.mapper.add("<" + tagName + Joiner.on("").join(attributes) + "/>");
+ return writer;
+ }
+
+ @Override
+ public StartTag named(FullyQualifiedName key) {
+ return named(key.name());
+ }
+
+ @Override
+ public StartTag named(String name) {
+ return createAttribute(null, "name", false).setTo(name);
+ }
+
+ @Override
+ public StartTag addAttributesFrom(Iterable<Entry<String, String>> entries) {
+ StartTag tag = this;
+ for (Entry<String, String> entry : entries) {
+ tag = tag.attribute(entry.getKey()).setTo(entry.getValue());
+ }
+ return tag;
+ }
+ }
+
+ /** Intermediate class that provides methods to generate string xml definitions. */
+ static class StringValuesResourceDefinition implements ValuesResourceDefinition {
+ private final SegmentMapper mapper;
+ private final Deque<String> tagStack = new ArrayDeque<>();
+
+ public StringValuesResourceDefinition(SegmentMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public StartTag startTag(String prefix, String localName) {
+ final String tagName = maybePrefixName(prefix, localName);
+ return new StringStartTag(this, tagName);
+ }
+
+ @Override
+ public StartTag startTag(String localName) {
+ return startTag(null, localName);
+ }
+
+ @Override
+ public StartTag startTag(QName name) {
+ return startTag(name.getPrefix().isEmpty() ? null : name.getPrefix(), name.getLocalPart());
+ }
+
+ @Override
+ public ValuesResourceDefinition adopt(FullyQualifiedName fqn) {
+ mapper.adopt(fqn);
+ return this;
+ }
+
+ @Override
+ public ValuesResourceDefinition addCharactersOf(String characters) {
+ mapper.add(characters);
+ return this;
+ }
+
+ @Override
+ public ValuesResourceDefinition endTag() {
+ Preconditions.checkArgument(!tagStack.isEmpty(),
+ "Unable to endTag, as no tag has been started.");
+ mapper.add("</" + tagStack.pop() + ">");
+ return this;
+ }
+
+ @Override
+ public void save() {
+ Preconditions.checkArgument(tagStack.isEmpty(), "Unfinished tags %s", tagStack);
+ mapper.finish();
+ }
+
+ @Override
+ public StartTag startItemTag() {
+ return startTag("item");
+ }
+ }
+
+ /** Maps {@link Segment}s to a {@link Multimap} via a {@link FullyQualifiedName} key. */
+ private static class SegmentMapper {
+ private List<String> currentLines = new ArrayList<>();
+
+ private final Multimap<FullyQualifiedName, Segment> segmentStore;
+ final FullyQualifiedName segmentKey;
+
+ private final Set<FullyQualifiedName> adopted;
+
+ private SegmentMapper(
+ Set<FullyQualifiedName> adopted,
+ Multimap<FullyQualifiedName, Segment> segmentStore,
+ FullyQualifiedName segmentKey) {
+ this.adopted = adopted;
+ this.segmentStore = segmentStore;
+ this.segmentKey = segmentKey;
+ }
+
+ static SegmentMapper create(
+ Multimap<FullyQualifiedName, Segment> segmentStore,
+ Set<FullyQualifiedName> adopted,
+ FullyQualifiedName segmentKey,
+ Path source) {
+ Preconditions.checkNotNull(source);
+ segmentStore.put(segmentKey, new SourceSegment(source));
+ return new SegmentMapper(adopted, segmentStore, segmentKey);
+ }
+
+ /** Adds a string to the current {@link StringsSegment}. */
+ SegmentMapper add(String line) {
+ currentLines.add(line);
+ return this;
+ }
+
+ /** Closes current {@link StringsSegment} and adds a {@link ReferenceSegment} to the store. */
+ SegmentMapper adopt(FullyQualifiedName adoptedKey) {
+ segmentStore.put(segmentKey, new StringsSegment(currentLines));
+ adopted.add(adoptedKey);
+ segmentStore.put(segmentKey, new ReferenceSegment(adoptedKey, segmentStore));
+ currentLines = new ArrayList<>();
+ return this;
+ }
+
+ /** Ends the mapping for the current key. */
+ void finish() {
+ segmentStore.put(segmentKey, new StringsSegment(currentLines));
+ }
+ }
+
+ /** Base interface for writing information to a {@link Writer}. */
+ private static interface Segment {
+ /**
+ * Writes the segment contents to a Writer
+ *
+ * @param previousSource The source Path of the last Segment to be written. This can be used to
+ * omit source annotations from the Writer is the last source matches the current one.
+ * @param writer The writer of the segments.
+ * @return The source Path of this segment for the next segment.
+ * @throws IOException thrown by the writer.
+ */
+ Path write(Path previousSource, @Nullable Writer writer) throws IOException;
+ }
+
+ /** Represents a reference to another list of {@link Segment}s from the {@link Multimap}. */
+ private static class ReferenceSegment implements Segment {
+ private final FullyQualifiedName fqn;
+ private final Multimap<FullyQualifiedName, Segment> segmentsByName;
+
+ public ReferenceSegment(
+ FullyQualifiedName fqn, Multimap<FullyQualifiedName, Segment> segmentsByName) {
+ this.fqn = fqn;
+ this.segmentsByName = segmentsByName;
+ }
+
+ @Override
+ public Path write(Path previousSource, Writer writer) throws IOException {
+ Path source = previousSource;
+ Preconditions.checkArgument(segmentsByName.containsKey(fqn), "%s has no segment in %s",
+ fqn.toPrettyString(), segmentsByName.keySet());
+ for (Segment s : segmentsByName.get(fqn)) {
+ // not recording the source
+ source = s.write(source, writer);
+ }
+ return source;
+ }
+ }
+
+ /** A simple container for a list of strings to be written. */
+ private static class StringsSegment implements Segment {
+ private final List<String> lines;
+
+ public StringsSegment(List<String> lines) {
+ this.lines = lines;
+ }
+
+ @Override
+ public Path write(Path previousSource, Writer writer) throws IOException {
+ for (String line : lines) {
+ writer.write(line);
+ writer.write(LINE_END);
+ }
+ return previousSource;
+ }
+ }
+
+ /** Represents a resource source annotation to be written as xml. */
+ private static class SourceSegment implements Segment {
+
+ private final Path source;
+
+ SourceSegment(Path source) {
+ this.source = source;
+ }
+
+ @Override
+ public Path write(Path previousSource, Writer writer) throws IOException {
+ // If sources are equal don't write a new source annotation.
+ if (source.equals(previousSource)) {
+ return previousSource;
+ }
+ writer.write(String.format("<!-- %s -->", source));
+ writer.write(LINE_END);
+ writer.write("<eat-comment/>");
+ writer.write(LINE_END);
+ return source;
+ }
+ }
}
diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java
index ce9b85394d..2aa469dc8a 100644
--- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java
+++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java
@@ -15,13 +15,16 @@ package com.google.devtools.build.android;
import com.android.ide.common.res2.MergingException;
+import java.io.Flushable;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.Map.Entry;
-/**
- * An interface for visiting android data for writing.
- */
-public interface AndroidDataWritingVisitor {
+import javax.annotation.CheckReturnValue;
+import javax.xml.namespace.QName;
+
+/** An interface for visiting android data for writing. */
+public interface AndroidDataWritingVisitor extends Flushable {
/**
* Copies the AndroidManifest to the destination directory.
*/
@@ -54,5 +57,110 @@ public interface AndroidDataWritingVisitor {
* @param xmlFragment the xml fragment as an Iterable<String> which allows lazy generation.
*/
// TODO(corysmith): Change this to pass in a xml writer. Safer all around.
+ @Deprecated
void writeToValuesXml(FullyQualifiedName key, Iterable<String> xmlFragment);
+
+ /**
+ * Provides a fluent interface to generate an xml resource for the values directory.
+ *
+ * <p>Example usage:
+ * <code>
+ * writer.define(key)
+ * .derivedFrom(source)
+ * .startTag(tagName)
+ * .named(key)
+ * .closeTag()
+ * .write(stringValue)
+ * .endTag()
+ * .save();
+ * </code>
+ */
+ // Check return value will ensure that the value is finished being written.
+ @CheckReturnValue
+ ValueResourceDefinitionMetadata define(FullyQualifiedName fqn);
+
+ /** Represents the xml values resource meta data. */
+ @CheckReturnValue
+ interface ValueResourceDefinitionMetadata {
+ ValuesResourceDefinition derivedFrom(Path source);
+ }
+
+ /** Fluent interface to define the xml value for a {@link FullyQualifiedName}. */
+ @CheckReturnValue
+ interface ValuesResourceDefinition {
+ /** Starts an xml tag with a prefix and localName. */
+ StartTag startTag(String prefix, String localName);
+
+ /** Starts an xml tag with a localName. */
+ StartTag startTag(String localName);
+
+ /** Starts an xml tag with a QName. */
+ StartTag startTag(QName name);
+
+ /** Starts an xml tag with the name "item" */
+ StartTag startItemTag();
+
+ /**
+ * Takes another values xml resource and writes it as a child tag here.
+ *
+ * <p>This allows xml elements from other {@link XmlResourceValue} to be moved in the stream.
+ * Currently, this is only necessary for {@link StyleableXmlResourceValue} which can have {@link
+ * AttrXmlResourceValue} defined as child elements (yet, they are merged and treated as
+ * independent resources.)
+ *
+ * @param fqn The {@link FullyQualifiedName} of the {@link XmlResourceValue} to be adopted. This
+ * resource doesn't have to be defined for the adopt invocation, but it must exist when
+ * {@link AndroidDataWritingVisitor#flush()} is called.
+ * @return The current definition.
+ */
+ ValuesResourceDefinition adopt(FullyQualifiedName fqn);
+
+ /** Adds a string as xml characters to the definition. */
+ ValuesResourceDefinition addCharactersOf(String characters);
+
+ /** Ends the last {@link StartTag}. */
+ ValuesResourceDefinition endTag();
+
+ /** Saves and validates the xml resource definition. */
+ void save();
+ }
+
+ /** Represents the start of opening tag of a resource xml. */
+ @CheckReturnValue
+ interface StartTag {
+ /** Adds name="{@link FullyQualifiedName}#name()" attribute. */
+ StartTag named(FullyQualifiedName key);
+ /** Adds "name" attribute to the {@link StartTag}. */
+ StartTag named(String key);
+ /** Adds all the {@link Entry} as key="value" to the {@link StartTag}. */
+ StartTag addAttributesFrom(Iterable<Entry<String, String>> entries);
+ /** Starts an attribute of prefix:name. */
+ Attribute attribute(String prefix, String name);
+ /** Starts an attribute of name. */
+ Attribute attribute(String string);
+ /** Indicates the next attribute will only be written if the value is not null. */
+ Optional optional();
+ /** Closes the {@link StartTag} as ">" */
+ ValuesResourceDefinition closeTag();
+ /** Closes the {@link StartTag} as "/>", indicating it is a unary xml element. */
+ ValuesResourceDefinition closeUnaryTag();
+ }
+
+ /** Adjective for an optional attribute. */
+ @CheckReturnValue
+ interface Optional {
+ /** Starts an attribute of prefix:name. */
+ Attribute attribute(String prefix, String name);
+ /** Starts an attribute of name. */
+ Attribute attribute(String string);
+ }
+
+ /** Represents an xml attribute of a start tag. */
+ @CheckReturnValue
+ interface Attribute {
+ /** Sets the attribute value. */
+ StartTag setTo(String value);
+ /** Sets the attributes values to {@linkplain FullyQualifiedName#name()}. */
+ StartTag setTo(FullyQualifiedName fqn);
+ }
}