Java tutorial
/* * Copyright 2016 Development Entropy (deventropy.org) Contributors * * 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 org.deventropy.shared.utils; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; 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.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.jar.JarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; import org.apache.commons.compress.utils.Charsets; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * Utility class providing convenience methods to create Zip/Jar archives for entire directories. * * <p>The functionality implemented in this class is rudimentary at this time, and does not support: * <ul> * <li>Compression Level</li> * <li>Compression Method</li> * <li>Advanced manifest manipulation (Jar files)</li> * <li>Jar signing</li> * <li>Filtering files out</li> * </ul> * * <h2>Creating a Zip Archive</h2> * To create a zip archive from a directory <code>/project/data/source</code> into a file * <code>/project/data/source.zip</code>, use the {@link #createZipArchiveOfDirectory(String, File, String)} as: * <pre> * DirectoryArchiverUtil.createJarArchiveOfDirectory("/project/data/source", "/project/data/source.zip", null); * </pre> * * <p>This will archive all the contents of the source folder recursively in the zip file created. Immediate children * of the source folder will be at the root of the zip file. The current implementation does not traverse down symbolic * links, and they will be excluded. * * <h3>Nesting contents in the archive</h3> * The implementation supports nesting contents from the source one or more directories down in the archive file * created. Working on the example above, if the code were invoked as: * <pre> * DirectoryArchiverUtil.createJarArchiveOfDirectory("/project/data/source", "/project/data/source.zip", "test/one"); * </pre> * * <p>This will cause any files or directories which were immediate children of the source folder to appear nested under * directories <code>test/one</code> in the archive (or when the archive is inflated). So * <code>/project/data/source/file.txt</code> will appear at <code>test/one/file.txt</code> in the archive. * * <h2>Creating a Jar Archive</h2> * Jar files are created almost identical to the Zip files above, with the additional functionality of a very * rudimentary Manifest (<code>META-INF/MANIFEST.MF</code>) file is added to the archive with just the Manifest Version * property set. * * <p>To create Jar files, use the {@link #createJarArchiveOfDirectory(String, File, String)} method instead. * * <p>This class uses ideas expressed by user Gili on a StackOverflow question: * <a href="http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file"> * How to use JarOutputStream to create a JAR file?</a> * * @author Bindul Bhowmik */ public final class DirectoryArchiverUtil { private static final String DEFAULT_MANIFEST_VERSION = "1.0"; private static final String ARCHIVE_PATH_SEPARATOR = "/"; private static final String WIN_PATH_SEPARATOR = "\\"; private static final String UTF_8_NAME = Charsets.UTF_8.name(); private static final Logger LOG = LogManager.getLogger(DirectoryArchiverUtil.class); private DirectoryArchiverUtil() { // Utility class } /** * Create a zip archive with all the contents of the directory. Optionally push the contents down a directory level * or two. * * @param archiveFile The final archive file location. The file location must be writable. * @param srcDirectory The source directory. * @param rootPathPrefix The root prefix. Multiple directory parts should be separated by <code>/</code>. * @throws IOException Exception reading the source directory or writing to the destination file. */ public static void createZipArchiveOfDirectory(final String archiveFile, final File srcDirectory, final String rootPathPrefix) throws IOException { createArchiveOfDirectory(archiveFile, srcDirectory, rootPathPrefix, ArchiveStreamFactory.ZIP, UTF_8_NAME, null); } /** * Create a Jar archive with all the contents of the directory. Optionally push the contents down a directory level * or two. A Manifest file is automatically added. * * @param archiveFile The final archive file location. The file location must be writable. * @param srcDirectory The source directory. * @param rootPathPrefix The root prefix. Multiple directory parts should be separated by <code>/</code>. * @throws IOException Exception reading the source directory or writing to the destination file. */ public static void createJarArchiveOfDirectory(final String archiveFile, final File srcDirectory, final String rootPathPrefix) throws IOException { createArchiveOfDirectory(archiveFile, srcDirectory, rootPathPrefix, ArchiveStreamFactory.JAR, UTF_8_NAME, new JarArchiverCreateProcessor()); } /** * Create a tar archive with all the contents of the directory. Optionally push the contents down a directory level * or two. * * @param archiveFile The final archive file location. The file location must be writable. * @param srcDirectory The source directory. * @param rootPathPrefix The root prefix. Multiple directory parts should be separated by <code>/</code>. * @throws IOException Exception reading the source directory or writing to the destination file. */ public static void createTarArchiveOfDirectory(final String archiveFile, final File srcDirectory, final String rootPathPrefix) throws IOException { createArchiveOfDirectory(archiveFile, srcDirectory, rootPathPrefix, ArchiveStreamFactory.TAR, null, new TarArchiverCreateProcessor(null)); } /** * Create a GZipped tar archive with all the contents of the directory. Optionally push the contents down a * directory level or two. * * @param archiveFile The final archive file location. The file location must be writable. * @param srcDirectory The source directory. * @param rootPathPrefix The root prefix. Multiple directory parts should be separated by <code>/</code>. * @throws IOException Exception reading the source directory or writing to the destination file. */ public static void createGZippedTarArchiveOfDirectory(final String archiveFile, final File srcDirectory, final String rootPathPrefix) throws IOException { createArchiveOfDirectory(archiveFile, srcDirectory, rootPathPrefix, ArchiveStreamFactory.TAR, null, new TarArchiverCreateProcessor(CompressorStreamFactory.GZIP)); } private static void createArchiveOfDirectory(final String archiveFile, final File srcDirectory, final String rootPathPrefix, final String archiveStreamFactoryConstant, final String encoding, final ArchiverCreateProcessor archiverCreateProcessorIn) throws IOException { /* * NOTE ON CHARSET ENCODING: Traditionally the ZIP archive format uses CodePage 437 as encoding for file name, * which is not sufficient for many international character sets. * Over time different archivers have chosen different ways to work around the limitation - the java.util.zip * packages simply uses UTF-8 as its encoding for example. * Ant has been offering the encoding attribute of the zip and unzip task as a way to explicitly specify the * encoding to use (or expect) since Ant 1.4. It defaults to the platform's default encoding for zip and UTF-8 * for jar and other jar-like tasks (war, ear, ...) as well as the unzip family of tasks. */ final ArchiverCreateProcessor archiveCreateProcessor = (null != archiverCreateProcessorIn) ? archiverCreateProcessorIn : new ArchiverCreateProcessor(); ArchiveOutputStream aos = null; try { final ArchiveStreamFactory archiveStreamFactory = new ArchiveStreamFactory(encoding); final FileOutputStream archiveFileOutputStream = new FileOutputStream(archiveFile); final OutputStream decoratedArchiveFileOutputStream = archiveCreateProcessor .decorateFileOutputStream(archiveFileOutputStream); aos = archiveStreamFactory.createArchiveOutputStream(archiveStreamFactoryConstant, decoratedArchiveFileOutputStream); archiveCreateProcessor.processArchiverPostCreate(aos, encoding); final String normalizedRootPathPrefix = (null == rootPathPrefix || rootPathPrefix.isEmpty()) ? "" : normalizeName(rootPathPrefix, true); if (!normalizedRootPathPrefix.isEmpty()) { final ArchiveEntry archiveEntry = aos.createArchiveEntry(srcDirectory, normalizedRootPathPrefix); aos.putArchiveEntry(archiveEntry); aos.closeArchiveEntry(); } final Path srcRootPath = Paths.get(srcDirectory.toURI()); final ArchiverFileVisitor visitor = new ArchiverFileVisitor(srcRootPath, normalizedRootPathPrefix, aos); Files.walkFileTree(srcRootPath, visitor); aos.flush(); } catch (ArchiveException e) { throw new IOException("Error creating archive", e); } finally { if (null != aos) { aos.close(); } } } private static String normalizeName(final String path, final boolean isDirectory) { String normalizedPath = path.replace(WIN_PATH_SEPARATOR, ARCHIVE_PATH_SEPARATOR); if (isDirectory && !normalizedPath.endsWith(ARCHIVE_PATH_SEPARATOR)) { normalizedPath += ARCHIVE_PATH_SEPARATOR; } return normalizedPath; } private static final class ArchiverFileVisitor extends SimpleFileVisitor<Path> { private final Path sourceRootPath; private final String normalizedRootPathPrefix; private final ArchiveOutputStream archiveOutputStream; private ArchiverFileVisitor(final Path sourceRootPath, final String normalizedRootPathPrefix, final ArchiveOutputStream archiveOutputStream) { this.normalizedRootPathPrefix = normalizedRootPathPrefix; this.sourceRootPath = sourceRootPath; this.archiveOutputStream = archiveOutputStream; } /* (non-Javadoc) * @see java.nio.file.FileVisitor#preVisitDirectory(java.lang.Object, * java.nio.file.attribute.BasicFileAttributes) */ @Override public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException { // Create a zip entry for the directory final Path relativeSourcePath = sourceRootPath.relativize(dir); if (relativeSourcePath.toString().isEmpty()) { // Per documentation in Path, the relative path is not NULL // Special case for the root return FileVisitResult.CONTINUE; } final String relativeDestinationPath = normalizeName(normalizedRootPathPrefix + relativeSourcePath, true); LOG.trace("Creating zip / jar entry for directory {} at {}", dir, relativeDestinationPath); final ArchiveEntry archiveEntry = archiveOutputStream.createArchiveEntry(dir.toFile(), relativeDestinationPath); archiveOutputStream.putArchiveEntry(archiveEntry); archiveOutputStream.closeArchiveEntry(); return FileVisitResult.CONTINUE; } /* (non-Javadoc) * @see java.nio.file.FileVisitor#visitFile(java.lang.Object, java.nio.file.attribute.BasicFileAttributes) */ @Override public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { // Add the file to the zip final Path relativeSourcePath = sourceRootPath.relativize(file); final String relativeDestinationPath = normalizeName(normalizedRootPathPrefix + relativeSourcePath, false); LOG.trace("Creating zip / jar entry for file {} at {}", file, relativeDestinationPath); final ArchiveEntry archiveEntry = archiveOutputStream.createArchiveEntry(file.toFile(), relativeDestinationPath); archiveOutputStream.putArchiveEntry(archiveEntry); Files.copy(file, archiveOutputStream); archiveOutputStream.closeArchiveEntry(); return FileVisitResult.CONTINUE; } } private static class ArchiverCreateProcessor { protected OutputStream decorateFileOutputStream(final FileOutputStream archiveFileOutputStream) throws IOException { return archiveFileOutputStream; } protected void processArchiverPostCreate(final ArchiveOutputStream archiveOutputStream, final String charset) throws IOException { // Default implementation does nothing. } } private static class JarArchiverCreateProcessor extends ArchiverCreateProcessor { @Override protected void processArchiverPostCreate(final ArchiveOutputStream archiveOutputStream, final String charset) throws IOException { super.processArchiverPostCreate(archiveOutputStream, charset); // Wrute the Jar Manifest file META-INF/MANIFEST.MF archiveOutputStream.putArchiveEntry(new JarArchiveEntry(JarFile.MANIFEST_NAME)); final Manifest manifest = new Manifest(); // Manifest-Version: 1.0 manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, DEFAULT_MANIFEST_VERSION); manifest.write(new BufferedOutputStream(archiveOutputStream)); archiveOutputStream.closeArchiveEntry(); } } private static class TarArchiverCreateProcessor extends ArchiverCreateProcessor { private final String compressor; protected TarArchiverCreateProcessor(final String compressor) { this.compressor = compressor; } @Override protected OutputStream decorateFileOutputStream(final FileOutputStream archiveFileOutputStream) throws IOException { OutputStream returnStream = super.decorateFileOutputStream(archiveFileOutputStream); if (null != compressor) { try { returnStream = new CompressorStreamFactory().createCompressorOutputStream(compressor, new BufferedOutputStream(archiveFileOutputStream)); } catch (CompressorException e) { throw new IOException("Error wrapping the file into a Compressed stream", e); } } return returnStream; } @Override protected void processArchiverPostCreate(final ArchiveOutputStream archiveOutputStream, final String charset) throws IOException { super.processArchiverPostCreate(archiveOutputStream, charset); final TarArchiveOutputStream tarArchiveOutputStream = (TarArchiveOutputStream) archiveOutputStream; tarArchiveOutputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); tarArchiveOutputStream.setAddPaxHeadersForNonAsciiNames(true); } } }