/* * Copyright © 2014 Cask Data, 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 co.cask.cdap.common.lang.jar; import co.cask.cdap.common.io.Locations; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import com.google.common.io.Files; import com.google.common.io.InputSupplier; import com.google.common.io.OutputSupplier; import org.apache.twill.filesystem.Location; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; /** * Utility functions that operate on bundle jars. */ // TODO: remove this -- not sure how to refactor though public class BundleJarUtil { /** * Load the manifest inside the given jar. * * @param jarLocation Location of the jar file. * @return The manifest inside the jar file or {@code null} if no manifest inside the jar file. * @throws IOException if failed to load the manifest. */ public static Manifest getManifest(Location jarLocation) throws IOException { URI uri = jarLocation.toURI(); // Small optimization if the location is local if ("file".equals(uri.getScheme())) { try (JarFile jarFile = new JarFile(new File(uri))) { return jarFile.getManifest(); } } // Otherwise, need to search it with JarInputStream try (JarInputStream is = new JarInputStream(new BufferedInputStream(jarLocation.getInputStream()))) { // This only looks at the first entry, which if is created with jar util, then it'll be there. Manifest manifest = is.getManifest(); if (manifest != null) { return manifest; } // Otherwise, slow path. Need to goes through the entries JarEntry jarEntry = is.getNextJarEntry(); while (jarEntry != null) { if (JarFile.MANIFEST_NAME.equals(jarEntry.getName())) { return new Manifest(is); } jarEntry = is.getNextJarEntry(); } } return null; } /** * Returns an {@link InputSupplier} for a given entry. This avoids unjar the whole file to just get one entry. * However, to get many entries, unjar would be more efficient. Also, the jar file is scanned every time the * {@link InputSupplier#getInput()} is invoked. * * @param jarLocation Location of the jar file. * @param entryName Name of the entry to fetch * @return An {@link InputSupplier}. */ public static InputSupplier<InputStream> getEntry(final Location jarLocation, final String entryName) throws IOException { Preconditions.checkArgument(jarLocation != null); Preconditions.checkArgument(entryName != null); final URI uri = jarLocation.toURI(); // Small optimization if the location is local if ("file".equals(uri.getScheme())) { return new InputSupplier<InputStream>() { @Override public InputStream getInput() throws IOException { final JarFile jarFile = new JarFile(new File(uri)); ZipEntry entry = jarFile.getEntry(entryName); if (entry == null) { throw new IOException("Entry not found for " + entryName); } return new FilterInputStream(jarFile.getInputStream(entry)) { @Override public void close() throws IOException { try { super.close(); } finally { jarFile.close(); } } }; } }; } // Otherwise, use JarInputStream return new InputSupplier<InputStream>() { @Override public InputStream getInput() throws IOException { JarInputStream is = new JarInputStream(jarLocation.getInputStream()); JarEntry entry = is.getNextJarEntry(); while (entry != null) { if (entryName.equals(entry.getName())) { return is; } entry = is.getNextJarEntry(); } Closeables.closeQuietly(is); throw new IOException("Entry not found for " + entryName); } }; } /** * Creates an JAR including all the files present in the given input. Same as calling * {@link #createArchive(File, OutputSupplier)} with a {@link JarOutputStream} created in the {@link OutputSupplier}. */ public static void createJar(File input, final File output) throws IOException { createArchive(input, new OutputSupplier<JarOutputStream>() { @Override public JarOutputStream getOutput() throws IOException { return new JarOutputStream(new BufferedOutputStream(new FileOutputStream(output))); } }); } /** * Creates an archive including all the files present in the given input. If the given input is a file, then it alone * is included in the archive. * * @param input input directory (or file) whose contents needs to be archived * @param outputSupplier An {@link OutputSupplier} for the archive content to be written to. * @throws IOException if there is failure in the archive creation */ public static void createArchive(File input, OutputSupplier<? extends ZipOutputStream> outputSupplier) throws IOException { final URI baseURI = input.toURI(); try (ZipOutputStream output = outputSupplier.getOutput()) { java.nio.file.Files.walkFileTree(input.toPath(), EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { URI uri = baseURI.relativize(dir.toUri()); if (!uri.getPath().isEmpty()) { output.putNextEntry(new ZipEntry(uri.getPath())); output.closeEntry(); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { URI uri = baseURI.relativize(file.toUri()); if (uri.getPath().isEmpty()) { // Only happen if the given "input" is a file. output.putNextEntry(new ZipEntry(file.toFile().getName())); } else { output.putNextEntry(new ZipEntry(uri.getPath())); } java.nio.file.Files.copy(file, output); output.closeEntry(); return FileVisitResult.CONTINUE; } }); } } /** * Unpack a jar file in the given location to a directory. * * @param jarLocation Location containing the jar file * @param destinationFolder Directory to expand into * @return The {@code destinationFolder} * @throws IOException If failed to expand the jar */ public static File unJar(Location jarLocation, File destinationFolder) throws IOException { Preconditions.checkArgument(jarLocation != null); return unJar(Locations.newInputSupplier(jarLocation), destinationFolder); } /** * Unpack a jar source to a directory. * * @param inputSupplier Supplier for the jar source * @param destinationFolder Directory to expand into * @return The {@code destinationFolder} * @throws IOException If failed to expand the jar */ public static File unJar(InputSupplier<? extends InputStream> inputSupplier, File destinationFolder) throws IOException { Preconditions.checkArgument(inputSupplier != null); Preconditions.checkArgument(destinationFolder != null); Preconditions.checkArgument(destinationFolder.canWrite()); destinationFolder.mkdirs(); Preconditions.checkState(destinationFolder.exists()); try (ZipInputStream input = new ZipInputStream(inputSupplier.getInput())) { unJar(input, destinationFolder); return destinationFolder; } } private static void unJar(ZipInputStream input, File targetDirectory) throws IOException { ZipEntry entry; while ((entry = input.getNextEntry()) != null) { File output = new File(targetDirectory, entry.getName()); if (entry.isDirectory()) { output.mkdirs(); } else { output.getParentFile().mkdirs(); ByteStreams.copy(input, Files.newOutputStreamSupplier(output)); } } } private BundleJarUtil() { } }