/* * Copyright 2014-present Facebook, Inc. * * 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.facebook.buck.jvm.java; import static com.facebook.buck.zip.ZipOutputStreams.HandleDuplicates.APPEND_TO_ZIP; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.zip.CustomJarOutputStream; import com.facebook.buck.zip.CustomZipEntry; import com.facebook.buck.zip.DeterministicManifest; import com.facebook.buck.zip.ZipOutputStreams; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.io.ByteStreams; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.regex.Pattern; import javax.annotation.Nullable; public class JarBuilder { public interface Observer { Observer IGNORING = new Observer() { @Override public void onDuplicateEntry(String jarFile, JarEntrySupplier entrySupplier) throws IOException {} @Override public void onEntryOmitted(String jarFile, JarEntrySupplier entrySupplier) throws IOException {} }; void onDuplicateEntry(String jarFile, JarEntrySupplier entrySupplier) throws IOException; void onEntryOmitted(String jarFile, JarEntrySupplier entrySupplier) throws IOException; } private final ProjectFilesystem filesystem; private Observer observer = Observer.IGNORING; @Nullable private Path outputFile; @Nullable private CustomJarOutputStream jar; @Nullable private String mainClass; @Nullable private Path manifestFile; private boolean shouldMergeManifests; private boolean shouldHashEntries; private Iterable<Pattern> blacklist = new ArrayList<>(); private List<JarEntryContainer> sourceContainers = new ArrayList<>(); private Set<String> alreadyAddedEntries = new HashSet<>(); public JarBuilder(ProjectFilesystem filesystem) { this.filesystem = filesystem; } public JarBuilder setObserver(Observer observer) { this.observer = observer; return this; } public JarBuilder setEntriesToJar(ImmutableSortedSet<Path> entriesToJar) { sourceContainers.clear(); entriesToJar .stream() .map(filesystem::getPathForRelativePath) .map(JarEntryContainer::of) .forEach(sourceContainers::add); return this; } public JarBuilder addEntryContainer(JarEntryContainer container) { sourceContainers.add(container); return this; } public JarBuilder setAlreadyAddedEntries(ImmutableSet<String> alreadyAddedEntries) { alreadyAddedEntries.forEach(this.alreadyAddedEntries::add); return this; } public JarBuilder setMainClass(String mainClass) { this.mainClass = mainClass; return this; } public JarBuilder setManifestFile(Path manifestFile) { this.manifestFile = manifestFile; return this; } public JarBuilder setShouldMergeManifests(boolean shouldMergeManifests) { this.shouldMergeManifests = shouldMergeManifests; return this; } public JarBuilder setShouldHashEntries(boolean shouldHashEntries) { this.shouldHashEntries = shouldHashEntries; return this; } public JarBuilder setEntryPatternBlacklist(Iterable<Pattern> blacklist) { this.blacklist = blacklist; return this; } public int createJarFile(Path outputFile) throws IOException { Preconditions.checkArgument(outputFile.isAbsolute()); try (CustomJarOutputStream jar = ZipOutputStreams.newJarOutputStream(outputFile, APPEND_TO_ZIP)) { jar.setEntryHashingEnabled(shouldHashEntries); return appendToJarFile(outputFile, jar); } } public int appendToJarFile(Path outputFile, CustomJarOutputStream jar) throws IOException { Preconditions.checkArgument(outputFile.isAbsolute()); this.outputFile = outputFile; this.jar = jar; // Write the manifest first. writeManifest(); for (JarEntryContainer sourceContainer : sourceContainers) { addEntriesToJar(sourceContainer); } if (mainClass != null && !mainClassPresent()) { throw new HumanReadableException("ERROR: Main class %s does not exist.", mainClass); } return 0; } private void writeManifest() throws IOException { DeterministicManifest manifest = jar.getManifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); if (shouldMergeManifests) { for (JarEntryContainer sourceContainer : sourceContainers) { Manifest readManifest = sourceContainer.getManifest(); if (readManifest != null) { merge(manifest, readManifest); } } } // Even if not merging manifests, we should include the one the user gave us. We do this last // so that values from the user overwrite values from merged manifests. if (manifestFile != null) { Path path = filesystem.getPathForRelativePath(manifestFile); try (InputStream stream = Files.newInputStream(path)) { Manifest readManifest = new Manifest(stream); merge(manifest, readManifest); } } // We may have merged manifests and over-written the user-supplied main class. Add it back. if (mainClass != null) { manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, mainClass); } jar.writeManifest(); } private boolean mainClassPresent() { String mainClassPath = classNameToPath(mainClass); return alreadyAddedEntries.contains(mainClassPath); } private String classNameToPath(String className) { return className.replace('.', '/') + ".class"; } public static String pathToClassName(String relativePath) { String entry = relativePath; if (relativePath.contains(".class")) { entry = relativePath.replace('/', '.').replace(".class", ""); } return entry; } private void addEntriesToJar(JarEntryContainer container) throws IOException { Iterable<JarEntrySupplier> entries = container.stream()::iterator; for (JarEntrySupplier entrySupplier : entries) { addEntryToJar(entrySupplier); } } private void addEntryToJar(JarEntrySupplier entrySupplier) throws IOException { CustomZipEntry entry = entrySupplier.getEntry(); String entryName = entry.getName(); // We already read the manifest. No need to read it again if (JarFile.MANIFEST_NAME.equals(entryName)) { return; } // Check if the entry belongs to the blacklist and it should be excluded from the Jar. if (shouldEntryBeRemovedFromJar(entrySupplier)) { return; } // We're in the process of merging a bunch of different jar files. These typically contain // just ".class" files and the manifest, but they can also include things like license files // from third party libraries and config files. We should include those license files within // the jar we're creating. Extracting them is left as an exercise for the consumer of the // jar. Because we don't know which files are important, the only ones we skip are // duplicate class files. if (!isDuplicateAllowed(entryName) && !alreadyAddedEntries.add(entryName)) { if (!entryName.endsWith("/")) { observer.onDuplicateEntry(String.valueOf(outputFile), entrySupplier); } return; } jar.putNextEntry(entry); try (InputStream entryInputStream = entrySupplier.getInputStreamSupplier().get()) { if (entryInputStream != null) { // Null stream means a directory ByteStreams.copy(entryInputStream, jar); } } jar.closeEntry(); } private boolean shouldEntryBeRemovedFromJar(JarEntrySupplier supplier) throws IOException { CustomZipEntry entry = supplier.getEntry(); String entryKey = pathToClassName(entry.getName()); for (Pattern pattern : blacklist) { if (pattern.matcher(entryKey).find()) { observer.onEntryOmitted(String.valueOf(outputFile), supplier); return true; } } return false; } /** * Merge entries from two Manifests together, with existing attributes being overwritten. * * @param into The Manifest to modify. * @param from The Manifest to copy from. */ private void merge(Manifest into, Manifest from) { Attributes attributes = from.getMainAttributes(); if (attributes != null) { for (Map.Entry<Object, Object> attribute : attributes.entrySet()) { into.getMainAttributes().put(attribute.getKey(), attribute.getValue()); } } Map<String, Attributes> entries = from.getEntries(); if (entries != null) { for (Map.Entry<String, Attributes> entry : entries.entrySet()) { Attributes existing = into.getAttributes(entry.getKey()); if (existing == null) { existing = new Attributes(); into.getEntries().put(entry.getKey(), existing); } existing.putAll(entry.getValue()); } } } private boolean isDuplicateAllowed(String name) { return !name.endsWith(".class") && !name.endsWith("/"); } }