org.deventropy.shared.utils.DirectoryArchiverUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.deventropy.shared.utils.DirectoryArchiverUtil.java

Source

/* 
 * 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);
        }
    }
}