diff options
author | 2017-01-31 08:10:11 +0000 | |
---|---|---|
committer | 2017-01-31 09:00:38 +0000 | |
commit | a006146535460b2fba821847e13816bec4540ba4 (patch) | |
tree | 1e81ed1ada9c14ceba678f1d12ed49a49c2eb55d | |
parent | de59c450f6c5af3204596e6b1d869476276fb59f (diff) |
Create a tool for building resource jars
Move the functionality for building resources jars out of JavaBuilder, in
preparation for building resource jars as a separate action.
--
PiperOrigin-RevId: 146086774
MOS_MIGRATED_REVID=146086774
8 files changed, 682 insertions, 0 deletions
diff --git a/src/java_tools/buildjar/BUILD b/src/java_tools/buildjar/BUILD index 880c13dc6f..b47b17ccc3 100644 --- a/src/java_tools/buildjar/BUILD +++ b/src/java_tools/buildjar/BUILD @@ -43,6 +43,7 @@ filegroup( "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:srcs", "//src/java_tools/buildjar/javatests/com/google/devtools/build/java/bazel:srcs", "//src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine:srcs", + "//src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar:srcs", ], visibility = ["//src:__pkg__"], ) diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD index 5e948d2ee9..d05bdeec40 100644 --- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/BUILD @@ -179,6 +179,7 @@ filegroup( srcs = glob(["**"]) + [ "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/genclass:srcs", "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/jarhelper:srcs", + "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar:srcs", "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:srcs", ], visibility = ["//src/java_tools/buildjar:__pkg__"], diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/BUILD new file mode 100644 index 0000000000..5ef3e21919 --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/BUILD @@ -0,0 +1,27 @@ +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/java_tools/buildjar/java/com/google/devtools/build/buildjar:__pkg__"], +) + +java_binary( + name = "ResourceJarBuilder", + main_class = "com.google.devtools.build.buildjar.resourcejar.ResourceJarBuilder", + runtime_deps = [":resourcejar"], +) + +java_library( + name = "resourcejar", + srcs = [ + "ResourceJarBuilder.java", + "ResourceJarOptions.java", + "ResourceJarOptionsParser.java", + ], + visibility = [ + "//src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar:__pkg__", + ], + deps = [ + "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/jarhelper", + "//third_party:guava", + ], +) diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilder.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilder.java new file mode 100644 index 0000000000..480f929b82 --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilder.java @@ -0,0 +1,170 @@ +// 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.buildjar.resourcejar; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.buildjar.jarhelper.JarCreator; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Constructs a jar file of Java resources. */ +public class ResourceJarBuilder implements Closeable { + + public static void main(String[] args) throws Exception { + build(ResourceJarOptionsParser.parse(Arrays.asList(args))); + } + + public static void build(ResourceJarOptions options) throws Exception { + try (ResourceJarBuilder builder = new ResourceJarBuilder(options)) { + builder.build(); + } + } + + /** Cache of opened zip filesystems. */ + private final Map<Path, FileSystem> filesystems = new HashMap<>(); + + private final ResourceJarOptions options; + + private ResourceJarBuilder(ResourceJarOptions options) { + this.options = options; + } + + public void build() throws IOException { + final JarCreator jar = new JarCreator(options.output()); + jar.setNormalize(true); + jar.setCompression(true); + + addResourceJars(jar, options.resourceJars()); + jar.addRootEntries(options.classpathResources()); + addResourceEntries(jar, options.resources()); + addMessageEntries(jar, options.messages()); + + jar.execute(); + } + + private void addResourceJars(final JarCreator jar, ImmutableList<String> resourceJars) + throws IOException { + for (String resourceJar : resourceJars) { + for (final Path root : getJarFileSystem(Paths.get(resourceJar)).getRootDirectories()) { + Files.walkFileTree( + root, + new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) + throws IOException { + // TODO(b/28452451): omit directories entries from jar files + if (dir.getNameCount() > 0) { + jar.addEntry(root.relativize(dir).toString(), dir); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) + throws IOException { + jar.addEntry(root.relativize(path).toString(), path); + return FileVisitResult.CONTINUE; + } + }); + } + } + } + + /** + * Adds a collection of resource entries. Each entry is a string composed of a pair of parts + * separated by a colon ':'. The name of the resource comes from the second part, and the path to + * the resource comes from the whole string with the colon replaced by a slash '/'. + * + * <pre> + * prefix:name => (name, prefix/name) + * </pre> + */ + private static void addResourceEntries(JarCreator jar, Collection<String> resources) + throws IOException { + for (String resource : resources) { + int colon = resource.indexOf(':'); + if (colon < 0) { + throw new IOException("" + resource + ": Illegal resource entry."); + } + String prefix = resource.substring(0, colon); + String name = resource.substring(colon + 1); + String path = colon > 0 ? prefix + "/" + name : name; + addEntryWithParents(jar, name, path); + } + } + + private static void addMessageEntries(JarCreator jar, List<String> messages) throws IOException { + for (String message : messages) { + int colon = message.indexOf(':'); + if (colon < 0) { + throw new IOException("" + message + ": Illegal message entry."); + } + String prefix = message.substring(0, colon); + String name = message.substring(colon + 1); + String path = colon > 0 ? prefix + "/" + name : name; + File messageFile = new File(path); + // Ignore empty messages. They get written by the translation importer + // when there is no translation for a particular language. + if (messageFile.length() != 0L) { + addEntryWithParents(jar, name, path); + } + } + } + + /** + * Adds an entry to the jar, making sure that all the parent dirs up to the base of {@code entry} + * are also added. + * + * @param entry the PathFragment of the entry going into the Jar file + * @param file the PathFragment of the input file for the entry + */ + @VisibleForTesting + static void addEntryWithParents(JarCreator creator, String entry, String file) { + while ((entry != null) && creator.addEntry(entry, file)) { + entry = new File(entry).getParent(); + file = new File(file).getParent(); + } + } + + private FileSystem getJarFileSystem(Path sourceJar) throws IOException { + FileSystem fs = filesystems.get(sourceJar); + if (fs == null) { + filesystems.put(sourceJar, fs = FileSystems.newFileSystem(sourceJar, null)); + } + return fs; + } + + @Override + public void close() throws IOException { + for (FileSystem fs : filesystems.values()) { + fs.close(); + } + } +} diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptions.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptions.java new file mode 100644 index 0000000000..bf1afe47c4 --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptions.java @@ -0,0 +1,113 @@ +// 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.buildjar.resourcejar; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; + +/** Resource jar builder options. */ +public class ResourceJarOptions { + private final String output; + private final ImmutableList<String> messages; + private final ImmutableList<String> resources; + private final ImmutableList<String> resourceJars; + private final ImmutableList<String> classpathResources; + + public ResourceJarOptions( + String output, + ImmutableList<String> messages, + ImmutableList<String> resources, + ImmutableList<String> resourceJars, + ImmutableList<String> classpathResources) { + this.output = checkNotNull(output); + this.messages = messages; + this.resources = resources; + this.resourceJars = resourceJars; + this.classpathResources = classpathResources; + } + + public String output() { + return output; + } + + /** + * Resources to include in the jar. + * + * <p>The format is {@code <prefix>:<name>}, where {@code <prefix>/<name>} is the path to the + * resource file, and {code <name>} is the relative name that will be used for the resource jar + * entry. + */ + public ImmutableList<String> resources() { + return resources; + } + + /** Message files to include in the resource jar. The format is the same as {@link #resources}. */ + public ImmutableList<String> messages() { + return messages; + } + + /** Jar files of resources to append to the resource jar. */ + public ImmutableList<String> resourceJars() { + return resourceJars; + } + + /** Files to include as top-level entries in the resource jar. */ + public ImmutableList<String> classpathResources() { + return classpathResources; + } + + public static Builder builder() { + return new Builder(); + } + + /** A builder for {@link ResourceJarOptions}. */ + public static class Builder { + private String output; + private ImmutableList<String> messages = ImmutableList.of(); + private ImmutableList<String> resources = ImmutableList.of(); + private ImmutableList<String> resourceJars = ImmutableList.of(); + private ImmutableList<String> classpathResources = ImmutableList.of(); + + public ResourceJarOptions build() { + return new ResourceJarOptions(output, messages, resources, resourceJars, classpathResources); + } + + public Builder setOutput(String output) { + this.output = checkNotNull(output); + return this; + } + + public Builder setMessages(ImmutableList<String> messages) { + this.messages = checkNotNull(messages); + return this; + } + + public Builder setResources(ImmutableList<String> resources) { + this.resources = checkNotNull(resources); + return this; + } + + public Builder setResourceJars(ImmutableList<String> resourceJars) { + this.resourceJars = checkNotNull(resourceJars); + return this; + } + + public Builder setClasspathResources(ImmutableList<String> classpathResources) { + this.classpathResources = checkNotNull(classpathResources); + return this; + } + } +} diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptionsParser.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptionsParser.java new file mode 100644 index 0000000000..f84b88394b --- /dev/null +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar/ResourceJarOptionsParser.java @@ -0,0 +1,121 @@ +// 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.buildjar.resourcejar; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayDeque; +import java.util.Deque; +import javax.annotation.Nullable; + +/** A command line options parser for {@link ResourceJarOptions}. */ +public class ResourceJarOptionsParser { + + /** + * Parses command line options into {@link ResourceJarOptions}, expanding any {@code @params} + * files. + */ + public static ResourceJarOptions parse(Iterable<String> args) throws IOException { + ResourceJarOptions.Builder builder = ResourceJarOptions.builder(); + parse(builder, args); + return builder.build(); + } + + /** + * Parses command line options into a {@link ResourceJarOptions.Builder}, expanding any + * {@code @params} files. + */ + public static void parse(ResourceJarOptions.Builder builder, Iterable<String> args) + throws IOException { + Deque<String> argumentDeque = new ArrayDeque<>(); + expandParamsFiles(argumentDeque, args); + parse(builder, argumentDeque); + } + + private static final Splitter ARG_SPLITTER = + Splitter.on(CharMatcher.breakingWhitespace()).omitEmptyStrings().trimResults(); + + /** + * Pre-processes an argument list, expanding arguments of the form {@code @filename} by reading + * the content of the file and appending whitespace-delimited options to {@code argumentDeque}. + */ + private static void expandParamsFiles(Deque<String> argumentDeque, Iterable<String> args) + throws IOException { + for (String arg : args) { + if (arg.isEmpty()) { + continue; + } + if (arg.startsWith("@") && !arg.startsWith("@@")) { + Path paramsPath = Paths.get(arg.substring(1)); + expandParamsFiles( + argumentDeque, ARG_SPLITTER.split(new String(Files.readAllBytes(paramsPath), UTF_8))); + } else { + argumentDeque.addLast(arg); + } + } + } + + private static void parse(ResourceJarOptions.Builder builder, Deque<String> argumentDeque) { + while (!argumentDeque.isEmpty()) { + String next = argumentDeque.pollFirst(); + switch (next) { + case "--output": + builder.setOutput(readOne(argumentDeque)); + break; + case "--messages": + builder.setMessages(readList(argumentDeque)); + break; + case "--resources": + builder.setResources(readList(argumentDeque)); + break; + case "--resource_jars": + builder.setResourceJars(readList(argumentDeque)); + break; + case "--classpath_resources": + builder.setClasspathResources(readList(argumentDeque)); + break; + default: + if (next.isEmpty() && !argumentDeque.isEmpty()) { + throw new IllegalArgumentException("unknown option: " + next); + } + } + } + } + + /** Returns the value of an option, or {@code null}. */ + @Nullable + private static String readOne(Deque<String> argumentDeque) { + if (argumentDeque.isEmpty() || argumentDeque.peekFirst().startsWith("-")) { + return null; + } + return argumentDeque.pollFirst(); + } + + /** Returns a list of option values. */ + private static ImmutableList<String> readList(Deque<String> argumentDeque) { + ImmutableList.Builder<String> result = ImmutableList.builder(); + while (!argumentDeque.isEmpty() && !argumentDeque.peekFirst().startsWith("--")) { + result.add(argumentDeque.pollFirst()); + } + return result.build(); + } +} diff --git a/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/BUILD b/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/BUILD new file mode 100644 index 0000000000..a587c7cb73 --- /dev/null +++ b/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/BUILD @@ -0,0 +1,17 @@ +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/java_tools/buildjar:__pkg__"], +) + +java_test( + name = "ResourceJarBuilderTest", + srcs = ["ResourceJarBuilderTest.java"], + deps = [ + "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/resourcejar", + "//third_party:guava", + "//third_party:junit4", + "//third_party:truth", + "//third_party/java/jdk/langtools:javac", + ], +) diff --git a/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilderTest.java b/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilderTest.java new file mode 100644 index 0000000000..8b89377053 --- /dev/null +++ b/src/java_tools/buildjar/javatests/com/google/devtools/build/buildjar/resourcejar/ResourceJarBuilderTest.java @@ -0,0 +1,232 @@ +// 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.buildjar.resourcejar; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** {@link com.google.devtools.build.buildjar.resourcejar.ResourceJarBuilder}Test. */ +@RunWith(JUnit4.class) +public class ResourceJarBuilderTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void options() throws IOException { + ResourceJarOptions options = + ResourceJarOptionsParser.parse( + ImmutableList.of( + "--output", + "resource.jar", + "--messages", + "m1", + "m2", + "--resources", + "r1", + "r2", + "--resource_jars", + "rj1", + "rj2", + "--classpath_resources", + "cr1", + "cr2")); + assertThat(options.output()).isEqualTo("resource.jar"); + assertThat(options.messages()).containsExactly("m1", "m2"); + assertThat(options.resources()).containsExactly("r1", "r2"); + assertThat(options.resourceJars()).containsExactly("rj1", "rj2"); + assertThat(options.classpathResources()).containsExactly("cr1", "cr2"); + } + + @Test + public void resourceJars() throws Exception { + File output = temporaryFolder.newFile("resources.jar"); + + File jar1 = temporaryFolder.newFile("jar1.jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar1))) { + jos.putNextEntry(new JarEntry("one/a.properties")); + jos.putNextEntry(new JarEntry("one/b.properties")); + } + + File jar2 = temporaryFolder.newFile("jar2.jar"); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar2))) { + jos.putNextEntry(new JarEntry("two/c.properties")); + jos.putNextEntry(new JarEntry("two/d.properties")); + } + + ResourceJarBuilder.build( + ResourceJarOptions.builder() + .setOutput(output.toString()) + .setResourceJars(ImmutableList.of(jar1.toString(), jar2.toString())) + .build()); + + List<String> entries = new ArrayList<>(); + try (JarFile jf = new JarFile(output)) { + Enumeration<JarEntry> jes = jf.entries(); + while (jes.hasMoreElements()) { + entries.add(jes.nextElement().getName()); + } + } + + assertThat(entries) + .containsExactly( + "META-INF/", + "META-INF/MANIFEST.MF", + "one/", + "one/a.properties", + "one/b.properties", + "two/", + "two/c.properties", + "two/d.properties") + .inOrder(); + } + + @Test + public void resources() throws Exception { + File output = temporaryFolder.newFile("resources.jar"); + + Path root = temporaryFolder.newFolder().toPath(); + + Path r1 = root.resolve("one/a.properties"); + Files.createDirectories(r1.getParent()); + Files.write(r1, "hello".getBytes(UTF_8)); + + Path r2 = root.resolve("two/b.properties"); + Files.createDirectories(r2.getParent()); + Files.write(r2, "goodbye".getBytes(UTF_8)); + + ResourceJarBuilder.build( + ResourceJarOptions.builder() + .setOutput(output.toString()) + .setResources( + ImmutableList.of( + root + ":" + root.relativize(r1), root + ":" + root.relativize(r2))) + .build()); + + List<String> entries = new ArrayList<>(); + try (JarFile jf = new JarFile(output)) { + Enumeration<JarEntry> jes = jf.entries(); + while (jes.hasMoreElements()) { + entries.add(jes.nextElement().getName()); + } + } + + assertThat(entries) + .containsExactly( + "META-INF/", + "META-INF/MANIFEST.MF", + "one/", + "one/a.properties", + "two/", + "two/b.properties"); + } + + @Test + public void rootEntries() throws Exception { + File output = temporaryFolder.newFile("resources.jar"); + + Path root = temporaryFolder.newFolder().toPath(); + + Path r1 = root.resolve("one/a.properties"); + Files.createDirectories(r1.getParent()); + Files.write(r1, "hello".getBytes(UTF_8)); + + Path r2 = root.resolve("two/b.properties"); + Files.createDirectories(r2.getParent()); + Files.write(r2, "goodbye".getBytes(UTF_8)); + + ResourceJarBuilder.build( + ResourceJarOptions.builder() + .setOutput(output.toString()) + .setClasspathResources(ImmutableList.of(r1.toString(), r2.toString())) + .build()); + + List<String> entries = new ArrayList<>(); + try (JarFile jf = new JarFile(output)) { + Enumeration<JarEntry> jes = jf.entries(); + while (jes.hasMoreElements()) { + entries.add(jes.nextElement().getName()); + } + } + + assertThat(entries) + .containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a.properties", "b.properties"); + } + + @Test + public void messages() throws Exception { + File output = temporaryFolder.newFile("resources.jar"); + + Path root = temporaryFolder.newFolder().toPath(); + + Path r1 = root.resolve("one/a.xmb"); + Files.createDirectories(r1.getParent()); + Files.write(r1, "hello".getBytes(UTF_8)); + + Path r2 = root.resolve("two/b.xmb"); + Files.createDirectories(r2.getParent()); + Files.write(r2, "goodbye".getBytes(UTF_8)); + + // empty messages are omitted + Path r3 = root.resolve("three/c.xmb"); + Files.createDirectories(r3.getParent()); + Files.write(r3, new byte[0]); + + ResourceJarBuilder.build( + ResourceJarOptions.builder() + .setOutput(output.toString()) + .setMessages( + ImmutableList.of( + root + ":" + root.relativize(r1), + root + ":" + root.relativize(r2), + root + ":" + root.relativize(r3))) + .build()); + + List<String> entries = new ArrayList<>(); + try (JarFile jf = new JarFile(output)) { + Enumeration<JarEntry> jes = jf.entries(); + while (jes.hasMoreElements()) { + entries.add(jes.nextElement().getName()); + } + } + + assertThat(entries) + .containsExactly( + "META-INF/", // + "META-INF/MANIFEST.MF", + "one/", + "one/a.xmb", + "two/", + "two/b.xmb") + .inOrder(); + } +} |