org.codehaus.plexus.archiver.zip.AbstractZipArchiver.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.plexus.archiver.zip.AbstractZipArchiver.java

Source

package org.codehaus.plexus.archiver.zip;

/**
 *
 * Copyright 2004 The Apache Software Foundation
 *
 *  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.
 */

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipEncoding;
import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
import org.apache.commons.compress.parallel.InputStreamSupplier;
import org.codehaus.plexus.archiver.AbstractArchiver;
import org.codehaus.plexus.archiver.ArchiveEntry;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.ResourceIterator;
import org.codehaus.plexus.archiver.UnixStat;
import org.codehaus.plexus.archiver.util.ResourceUtils;
import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
import org.codehaus.plexus.components.io.resources.PlexusIoResource;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;

import javax.annotation.WillClose;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Hashtable;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
import java.util.zip.CRC32;

import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
import static org.codehaus.plexus.archiver.util.Streams.fileOutputStream;

/**
 * @version $Revision$ $Date$
 */
@SuppressWarnings({ "NullableProblems", "UnusedDeclaration" })
public abstract class AbstractZipArchiver extends AbstractArchiver {

    private String comment;

    /**
     * Encoding to use for filenames, defaults to the platform's
     * default encoding.
     */
    private String encoding = "UTF8";

    private boolean doCompress = true;

    private boolean recompressAddedZips = true;

    private boolean doUpdate = false;

    // shadow of the above if the value is altered in execute
    private boolean savedDoUpdate = false;

    protected String archiveType = "zip";

    private boolean doFilesonly = false;

    protected final Hashtable<String, String> entries = new Hashtable<String, String>();

    protected final AddedDirs addedDirs = new AddedDirs();

    private static final long EMPTY_CRC = new CRC32().getValue();

    protected boolean doubleFilePass = false;

    protected boolean skipWriting = false;

    /**
     * @deprecated Use {@link Archiver#setDuplicateBehavior(String)} instead.
     */
    protected final String duplicate = Archiver.DUPLICATES_SKIP;

    /**
     * true when we are adding new files into the Zip file, as opposed
     * to adding back the unchanged files
     */
    protected boolean addingNewFiles = false;

    /**
     * Whether the file modification times will be rounded up to the
     * next even number of seconds.
     * <p/>
     * <p>Zip archives store file modification times with a
     * granularity of two seconds, so the times will either be rounded
     * up or down.  If you round down, the archive will always seem
     * out-of-date when you rerun the task, so the default is to round
     * up.  Rounding up may lead to a different type of problems like
     * JSPs inside a web archive that seem to be slightly more recent
     * than precompiled pages, rendering precompilation useless.</p>    *
     * <p/>
     * plexus-archiver chooses to round up.
     * <p/>
     * Java versions up to java7 round timestamp down, which means we add a heuristic value (which is slightly
     * questionable)
     * Java versions from 8 and up round timestamp up.
     * s
     */
    private static final boolean isJava7OrLower = Integer
            .parseInt(System.getProperty("java.version").split("\\.")[1]) <= 7;

    // Renamed version of original file, if it exists
    private File renamedFile = null;

    private File zipFile;

    private boolean success;

    private ConcurrentJarCreator zOut;

    protected ZipArchiveOutputStream zipArchiveOutputStream;

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public String getEncoding() {
        return encoding;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public void setCompress(boolean compress) {
        this.doCompress = compress;
    }

    public boolean isCompress() {
        return doCompress;
    }

    public boolean isRecompressAddedZips() {
        return recompressAddedZips;
    }

    public void setRecompressAddedZips(boolean recompressAddedZips) {
        this.recompressAddedZips = recompressAddedZips;
    }

    public void setUpdateMode(boolean update) {
        this.doUpdate = update;
        savedDoUpdate = doUpdate;
    }

    public boolean isInUpdateMode() {
        return doUpdate;
    }

    /**
     * If true, emulate Sun's jar utility by not adding parent directories;
     * optional, defaults to false.
     *
     * @param f true to emilate sun jar utility
     */
    public void setFilesonly(boolean f) {
        doFilesonly = f;
    }

    public boolean isFilesonly() {
        return doFilesonly;
    }

    protected void execute() throws ArchiverException, IOException {
        if (!checkForced()) {
            return;
        }

        if (doubleFilePass) {
            skipWriting = true;
            createArchiveMain();
            skipWriting = false;
            createArchiveMain();
        } else {
            createArchiveMain();
        }

        finalizeZipOutputStream(zOut);
    }

    protected void finalizeZipOutputStream(ConcurrentJarCreator zOut) throws IOException, ArchiverException {
    }

    private void createArchiveMain() throws ArchiverException, IOException {
        //noinspection deprecation
        if (!Archiver.DUPLICATES_SKIP.equals(duplicate)) {
            //noinspection deprecation
            setDuplicateBehavior(duplicate);
        }

        ResourceIterator iter = getResources();
        if (!iter.hasNext() && !hasVirtualFiles()) {
            throw new ArchiverException("You must set at least one file.");
        }

        zipFile = getDestFile();

        if (zipFile == null) {
            throw new ArchiverException("You must set the destination " + archiveType + "file.");
        }

        if (zipFile.exists() && !zipFile.isFile()) {
            throw new ArchiverException(zipFile + " isn't a file.");
        }

        if (zipFile.exists() && !zipFile.canWrite()) {
            throw new ArchiverException(zipFile + " is read-only.");
        }

        // Whether or not an actual update is required -
        // we don't need to update if the original file doesn't exist

        addingNewFiles = true;

        if (doUpdate && !zipFile.exists()) {
            doUpdate = false;
            getLogger().debug("ignoring update attribute as " + archiveType + " doesn't exist.");
        }

        success = false;

        if (doUpdate) {
            renamedFile = FileUtils.createTempFile("zip", ".tmp", zipFile.getParentFile());
            renamedFile.deleteOnExit();

            try {
                FileUtils.rename(zipFile, renamedFile);
            } catch (SecurityException e) {
                getLogger().debug(e.toString());
                throw new ArchiverException(
                        "Not allowed to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e);
            } catch (IOException e) {
                getLogger().debug(e.toString());
                throw new ArchiverException(
                        "Unable to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e);
            }
        }

        String action = doUpdate ? "Updating " : "Building ";

        getLogger().info(action + archiveType + ": " + zipFile.getAbsolutePath());

        if (!skipWriting) {
            zipArchiveOutputStream = new ZipArchiveOutputStream(
                    bufferedOutputStream(fileOutputStream(zipFile, "zip")));
            zipArchiveOutputStream.setEncoding(encoding);
            zipArchiveOutputStream
                    .setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NOT_ENCODEABLE);
            zipArchiveOutputStream
                    .setMethod(doCompress ? ZipArchiveOutputStream.DEFLATED : ZipArchiveOutputStream.STORED);

            zOut = new ConcurrentJarCreator(Runtime.getRuntime().availableProcessors());
        }
        initZipOutputStream(zOut);

        // Add the new files to the archive.
        addResources(iter, zOut);

        // If we've been successful on an update, delete the
        // temporary file
        if (doUpdate) {
            if (!renamedFile.delete()) {
                getLogger().warn("Warning: unable to delete temporary file " + renamedFile.getName());
            }
        }
        success = true;
    }

    /**
     * Add the given resources.
     *
     * @param resources the resources to add
     * @param zOut      the stream to write to
     */
    @SuppressWarnings({ "JavaDoc" })
    protected final void addResources(ResourceIterator resources, ConcurrentJarCreator zOut)
            throws IOException, ArchiverException {
        while (resources.hasNext()) {
            ArchiveEntry entry = resources.next();
            String name = entry.getName();
            name = name.replace(File.separatorChar, '/');

            if ("".equals(name)) {
                continue;
            }

            if (entry.getResource().isDirectory() && !name.endsWith("/")) {
                name = name + "/";
            }

            addParentDirs(entry, null, name, zOut);

            if (entry.getResource().isFile()) {
                zipFile(entry, zOut, name);
            } else {
                zipDir(entry.getResource(), zOut, name, entry.getMode(), encoding);
            }
        }
    }

    /**
     * Ensure all parent dirs of a given entry have been added.
     * <p/>
     * This method is computed in terms of the potentially remapped entry (that may be disconnected from the file system)
     * we do not *relly* know the entry's connection to the file system so establishing the attributes of the parents can
     * be impossible and is not really supported.
     */
    @SuppressWarnings({ "JavaDoc" })
    private void addParentDirs(ArchiveEntry archiveEntry, File baseDir, String entry, ConcurrentJarCreator zOut)
            throws IOException {
        if (!doFilesonly && getIncludeEmptyDirs()) {
            Stack<String> directories = addedDirs.asStringStack(entry);

            while (!directories.isEmpty()) {
                String dir = directories.pop();
                File f;
                if (baseDir != null) {
                    f = new File(baseDir, dir);
                } else {
                    f = new File(dir);
                }
                // the
                // At this point we could do something like read the atr
                final PlexusIoResource res = new AnonymousResource(f);
                zipDir(res, zOut, dir, archiveEntry.getDefaultDirMode(), encoding);
            }
        }
    }

    /**
      * Adds a new entry to the archive, takes care of duplicates as well.
      *  @param in                 the stream to read data for the entry from.
      * @param zOut               the stream to write to.
      * @param vPath              the name this entry shall have in the archive.
      * @param lastModified       last modification time for the entry.
      * @param fromArchive        the original archive we are copying this
      * @param symlinkDestination
      */
    @SuppressWarnings({ "JavaDoc" })
    protected void zipFile(InputStreamSupplier in, ConcurrentJarCreator zOut, String vPath, long lastModified,
            File fromArchive, int mode, String symlinkDestination) throws IOException, ArchiverException {
        getLogger().debug("adding entry " + vPath);

        entries.put(vPath, vPath);

        if (!skipWriting) {
            ZipArchiveEntry ze = new ZipArchiveEntry(vPath);
            setTime(ze, lastModified);

            ze.setMethod(doCompress ? ZipArchiveEntry.DEFLATED : ZipArchiveEntry.STORED);
            ze.setUnixMode(UnixStat.FILE_FLAG | mode);

            InputStream payload;
            if (ze.isUnixSymlink()) {
                ZipEncoding enc = ZipEncodingHelper.getZipEncoding(getEncoding());
                final byte[] bytes = enc.encode(symlinkDestination).array();
                payload = new ByteArrayInputStream(bytes);
                zOut.addArchiveEntry(ze, createInputStreamSupplier(payload));
            } else {
                zOut.addArchiveEntry(ze, wrappedRecompressor(ze, in));
            }
        }
    }

    private InputStream maybeSequence(byte[] header, int hdrBytes, InputStream in) {
        return hdrBytes > 0 ? new SequenceInputStream(new ByteArrayInputStream(header, 0, hdrBytes), in) : in;
    }

    private boolean isZipHeader(byte[] header) {
        return header[0] == 0x50 && header[1] == 0x4b && header[2] == 3 && header[3] == 4;
    }

    /**
     * Method that gets called when adding from java.io.File instances.
     * <p/>
     * <p>This implementation delegates to the six-arg version.</p>
     *  @param entry the file to add to the archive
     * @param zOut  the stream to write to
    * @param vPath the name this entry shall have in the archive
    */
    @SuppressWarnings({ "JavaDoc" })
    protected void zipFile(final ArchiveEntry entry, ConcurrentJarCreator zOut, String vPath)
            throws IOException, ArchiverException {
        final PlexusIoResource resource = entry.getResource();
        if (ResourceUtils.isSame(resource, getDestFile())) {
            throw new ArchiverException("A zip file cannot include itself");
        }

        final boolean b = entry.getResource() instanceof SymlinkDestinationSupplier;
        String symlinkTarget = b ? ((SymlinkDestinationSupplier) entry.getResource()).getSymlinkDestination()
                : null;
        InputStreamSupplier in = new InputStreamSupplier() {
            @Override
            public InputStream get() {
                try {
                    return entry.getInputStream();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        try {
            zipFile(in, zOut, vPath, resource.getLastModified(), null, entry.getMode(), symlinkTarget);
        } catch (IOException e) {
            throw new ArchiverException("IOException when zipping r" + entry.getName() + ": " + e.getMessage(), e);
        }
    }

    private void setTime(java.util.zip.ZipEntry zipEntry, long lastModified) {
        zipEntry.setTime(lastModified + (isJava7OrLower ? 1999 : 0));

        /*   Consider adding extended file stamp support.....
            
        X5455_ExtendedTimestamp ts =  new X5455_ExtendedTimestamp();
        ts.setModifyJavaTime(new Date(lastModified));
        if (zipEntry.getExtra() != null){
           // Uh-oh. What do we do now.
           throw new IllegalStateException("DIdnt expect to see xtradata here ?");
            
        }  else {
           zipEntry.setExtra(ts.getLocalFileDataData());
        }
          */
    }

    protected void zipDir(PlexusIoResource dir, ConcurrentJarCreator zOut, String vPath, int mode,
            String encodingToUse) throws IOException {
        if (addedDirs.update(vPath)) {
            return;
        }

        getLogger().debug("adding directory " + vPath);

        if (!skipWriting) {
            final boolean isSymlink = dir instanceof SymlinkDestinationSupplier;

            if (isSymlink && vPath.endsWith(File.separator)) {
                vPath = vPath.substring(0, vPath.length() - 1);
            }

            ZipArchiveEntry ze = new ZipArchiveEntry(vPath);

            /*
             * ZipOutputStream.putNextEntry expects the ZipEntry to
             * know its size and the CRC sum before you start writing
             * the data when using STORED mode - unless it is seekable.
             *
             * This forces us to process the data twice.
             */

            if (isSymlink) {
                mode = UnixStat.LINK_FLAG | mode;
            }

            if (dir != null && dir.isExisting()) {
                setTime(ze, dir.getLastModified());
            } else {
                // ZIPs store time with a granularity of 2 seconds, round up
                setTime(ze, System.currentTimeMillis());
            }
            if (!isSymlink) {
                ze.setSize(0);
                ze.setMethod(ZipArchiveEntry.STORED);
                // This is faintly ridiculous:
                ze.setCrc(EMPTY_CRC);
            }
            ze.setUnixMode(mode);

            if (!isSymlink) {
                zOut.addArchiveEntry(ze, createInputStreamSupplier(new ByteArrayInputStream("".getBytes())));
            } else {
                String symlinkDestination = ((SymlinkDestinationSupplier) dir).getSymlinkDestination();
                ZipEncoding enc = ZipEncodingHelper.getZipEncoding(encodingToUse);
                final byte[] bytes = enc.encode(symlinkDestination).array();
                ze.setMethod(ZipArchiveEntry.DEFLATED);
                zOut.addArchiveEntry(ze, createInputStreamSupplier(new ByteArrayInputStream(bytes)));
            }
        }
    }

    private InputStreamSupplier wrappedRecompressor(final ZipArchiveEntry ze, final InputStreamSupplier other) {

        return new InputStreamSupplier() {
            public InputStream get() {
                InputStream is = other.get();
                byte[] header = new byte[4];
                try {
                    int read = is.read(header);
                    boolean compressThis = doCompress;
                    if (!recompressAddedZips && isZipHeader(header)) {
                        compressThis = false;
                    }

                    ze.setMethod(compressThis ? ZipArchiveEntry.DEFLATED : ZipArchiveEntry.STORED);

                    return maybeSequence(header, read, is);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }
        };
    }

    protected InputStreamSupplier createInputStreamSupplier(final InputStream inputStream) {
        return new InputStreamSupplier() {
            public InputStream get() {
                return inputStream;
            }
        };
    }

    /**
     * Create an empty zip file
     *
     * @param zipFile The file
     * @return true for historic reasons
     */
    @SuppressWarnings({ "JavaDoc" })
    protected boolean createEmptyZip(File zipFile) throws ArchiverException {
        // In this case using java.util.zip will not work
        // because it does not permit a zero-entry archive.
        // Must create it manually.
        getLogger().info("Note: creating empty " + archiveType + " archive " + zipFile);
        OutputStream os = null;
        try {
            os = new FileOutputStream(zipFile);
            // Cf. PKZIP specification.
            byte[] empty = new byte[22];
            empty[0] = 80; // P
            empty[1] = 75; // K
            empty[2] = 5;
            empty[3] = 6;
            // remainder zeros
            os.write(empty);
        } catch (IOException ioe) {
            throw new ArchiverException("Could not create empty ZIP archive " + "(" + ioe.getMessage() + ")", ioe);
        } finally {
            IOUtil.close(os);
        }
        return true;
    }

    /**
     * Do any clean up necessary to allow this instance to be used again.
     * <p/>
     * <p>When we get here, the Zip file has been closed and all we
     * need to do is to reset some globals.</p>
     * <p/>
     * <p>This method will only reset globals that have been changed
     * during execute(), it will not alter the attributes or nested
     * child elements.  If you want to reset the instance so that you
     * can later zip a completely different set of files, you must use
     * the reset method.</p>
     *
     * @see #reset
     */
    protected void cleanUp() throws IOException {
        super.cleanUp();
        addedDirs.clear();
        entries.clear();
        addingNewFiles = false;
        doUpdate = savedDoUpdate;
        success = false;
        zOut = null;
        renamedFile = null;
        zipFile = null;
    }

    /**
     * Makes this instance reset all attributes to their default
     * values and forget all children.
     *
     * @see #cleanUp
     */
    public void reset() {
        setDestFile(null);
        //        duplicate = "add";
        archiveType = "zip";
        doCompress = true;
        doUpdate = false;
        doFilesonly = false;
        encoding = null;
    }

    /**
     * method for subclasses to override
     *
    * @param zOut The output stream
    */
    protected void initZipOutputStream(ConcurrentJarCreator zOut) throws ArchiverException, IOException {
    }

    /**
     * method for subclasses to override
     */
    public boolean isSupportingForced() {
        return true;
    }

    protected boolean revert(StringBuffer messageBuffer) {
        int initLength = messageBuffer.length();

        // delete a bogus ZIP file (but only if it's not the original one)
        if ((!doUpdate || renamedFile != null) && !zipFile.delete()) {
            messageBuffer.append(" (and the archive is probably corrupt but I could not delete it)");
        }

        if (doUpdate && renamedFile != null) {
            try {
                FileUtils.rename(renamedFile, zipFile);
            } catch (IOException e) {
                messageBuffer.append(" (and I couldn't rename the temporary file ");
                messageBuffer.append(renamedFile.getName());
                messageBuffer.append(" back)");
            }
        }

        return messageBuffer.length() == initLength;
    }

    protected void close() throws IOException {
        // Close the output stream.
        try {
            if (zipArchiveOutputStream != null) {
                zOut.writeTo(zipArchiveOutputStream);
                zipArchiveOutputStream.close();
            }
        } catch (IOException ex) {
            // If we're in this finally clause because of an
            // exception, we don't really care if there's an
            // exception when closing the stream. E.g. if it
            // throws "ZIP file must have at least one entry",
            // because an exception happened before we added
            // any files, then we must swallow this
            // exception. Otherwise, the error that's reported
            // will be the close() error, which is not the
            // real cause of the problem.
            if (success) {
                throw ex;
            }

        } catch (InterruptedException e) {
            IOException ex = new IOException("InterruptedException exception");
            ex.initCause(e.getCause());
            throw ex;
        } catch (ExecutionException e) {
            IOException ex = new IOException("Execution exception");
            ex.initCause(e.getCause());
            throw ex;
        }
    }

    protected String getArchiveType() {
        return archiveType;
    }

}