diff options
author | Googler <noreply@google.com> | 2016-06-29 15:49:52 +0000 |
---|---|---|
committer | Dmitry Lomov <dslomov@google.com> | 2016-06-30 11:40:54 +0000 |
commit | af2ea37ceab2300a6d40afe65c1e333dfbaa0a11 (patch) | |
tree | ebecae8914404203e3944f03f9b6a5b31bb11c3f /src | |
parent | 368cc56fb2baa3e21be4acdd2410a4ce8245de93 (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.java | 434 | ||||
-rw-r--r-- | src/tools/android/java/com/google/devtools/build/android/AndroidDataWritingVisitor.java | 116 |
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); + } } |