// 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.util.Objects.requireNonNull; 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; public ResourceJarBuilder(ResourceJarOptions options) { this.options = options; } public void build() throws IOException { requireNonNull(options.output()); final JarCreator jar = new JarCreator(options.output()); build(jar); jar.execute(); } public void build(JarCreator jar) throws IOException { jar.setNormalize(true); jar.setCompression(true); addResourceJars(jar, options.resourceJars()); jar.addRootEntries(options.classpathResources()); addResourceEntries(jar, options.resources()); addMessageEntries(jar, options.messages()); } 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(); } } }