aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java
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/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java
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/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidDataWriter.java434
1 files changed, 419 insertions, 15 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;
+ }
+ }
}