/*
 * SeekableXZInputStream
 *
 * Author: Lasse Collin <lasse.collin@tukaani.org>
 *
 * This file has been put into the public domain.
 * You can do whatever you want with this file.
 */

package org.tukaani.xz;

import java.util.Arrays;
import java.util.ArrayList;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.EOFException;
import org.tukaani.xz.common.DecoderUtil;
import org.tukaani.xz.common.StreamFlags;
import org.tukaani.xz.check.Check;
import org.tukaani.xz.index.IndexDecoder;
import org.tukaani.xz.index.BlockInfo;

/**
 * Decompresses a .xz file in random access mode.
 * This supports decompressing concatenated .xz files.
 *

 * Each .xz file consist of one or more Streams. Each Stream consist of zero
 * or more Blocks. Each Stream contains an Index of Streams' Blocks.
 * The Indexes from all Streams are loaded in RAM by a constructor of this
 * class. A typical .xz file has only one Stream, and parsing its Index will
 * need only three or four seeks.
 *

 * To make random access possible, the data in a .xz file must be splitted
 * into multiple Blocks of reasonable size. Decompression can only start at
 * a Block boundary. When seeking to an uncompressed position that is not at
 * a Block boundary, decompression starts at the beginning of the Block and
 * throws away data until the target position is reached. Thus, smaller Blocks
 * mean faster seeks to arbitrary uncompressed positions. On the other hand,
 * smaller Blocks mean worse compression. So one has to make a compromise
 * between random access speed and compression ratio.
 *

 * Implementation note: This class uses linear search to locate the correct
 * Stream from the data structures in RAM. It was the simplest to implement
 * and should be fine as long as there aren't too many Streams. The correct
 * Block inside a Stream is located using binary search and thus is fast
 * even with a huge number of Blocks.
 *
 *
Memory usage

 *

 * The amount of memory needed for the Indexes is taken into account when
 * checking the memory usage limit. Each Stream is calculated to need at
 * least 1 KiB of memory and each Block 16 bytes of memory, rounded up
 * to the next kibibyte. So unless the file has a huge number of Streams or
 * Blocks, these don't take significant amount of memory.
 *
 *
Creating random-accessible .xz files

 *

 * When using {@link XZOutputStream}, a new Block can be started by calling
 * its {@link XZOutputStream#endBlock() endBlock} method. If you know
 * that the decompressor will only need to seek to certain uncompressed
 * positions, it can be a good idea to start a new Block at (some of) these
 * positions (and only at these positions to get better compression ratio).
 *

 * liblzma in XZ Utils supports starting a new Block with
 * LZMA_FULL_FLUSH. XZ Utils 5.1.1alpha added threaded
 * compression which creates multi-Block .xz files. XZ Utils 5.1.1alpha
 * also added the option --block-size=SIZE to the xz command
 * line tool. XZ Utils 5.1.2alpha added a partial implementation of
 * --block-list=SIZES which allows specifying sizes of
 * individual Blocks.
 *
 * @see SeekableFileInputStream
 * @see XZInputStream
 * @see XZOutputStream
 */
public class SeekableXZInputStream extends SeekableInputStream {
    /**
     * Cache for big arrays.
     */
    private final ArrayCache arrayCache;

    /**
     * The input stream containing XZ compressed data.
     */
    private SeekableInputStream in;

    /**
     * Memory usage limit after the memory usage of the IndexDecoders have
     * been substracted.
     */
    private final int memoryLimit;

    /**
     * Memory usage of the IndexDecoders.
     * memoryLimit + indexMemoryUsage equals the original
     * memory usage limit that was passed to the constructor.
     */
    private int indexMemoryUsage = 0;

    /**
     * List of IndexDecoders, one for each Stream in the file.
     * The list is in reverse order: The first element is
     * the last Stream in the file.
     */
    private final ArrayList<IndexDecoder> streams
            = new ArrayList<IndexDecoder>();

    /**
     * Bitmask of all Check IDs seen.
     */
    private int checkTypes = 0;

    /**
     * Uncompressed size of the file (all Streams).
     */
    private long uncompressedSize = 0;

    /**
     * Uncompressed size of the largest XZ Block in the file.
     */
    private long largestBlockSize = 0;

    /**
     * Number of XZ Blocks in the file.
     */
    private int blockCount = 0;

    /**
     * Size and position information about the current Block.
     * If there are no Blocks, all values will be -1.
     */
    private final BlockInfo curBlockInfo;

    /**
     * Temporary (and cached) information about the Block whose information
     * is queried via getBlockPos and related functions.
     */
    private final BlockInfo queriedBlockInfo;

    /**
     * Integrity Check in the current XZ Stream. The constructor leaves
     * this to point to the Check of the first Stream.
     */
    private Check check;

    /**
     * Flag indicating if the integrity checks will be verified.
     */
    private final boolean verifyCheck;

    /**
     * Decoder of the current XZ Block, if any.
     */
    private BlockInputStream blockDecoder = null;

    /**
     * Current uncompressed position.
     */
    private long curPos = 0;

    /**
     * Target position for seeking.
     */
    private long seekPos;

    /**
     * True when seek(long) has been called but the actual
     * seeking hasn't been done yet.
     */
    private boolean seekNeeded = false;

    /**
     * True when end of the file was reached. This can be cleared by
     * calling seek(long).
     */
    private boolean endReached = false;

    /**
     * Pending exception from an earlier error.
     */
    private IOException exception = null;

    /**
     * Temporary buffer for read(). This avoids reallocating memory
     * on every read() call.
     */
    private final byte[] tempBuf = new byte[1];

    /**
     * Creates a new seekable XZ decompressor without a memory usage limit.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     */
    public SeekableXZInputStream(SeekableInputStream in)
            throws IOException {
        this(in, -1);
    }

    /**
     * Creates a new seekable XZ decompressor without a memory usage limit.
     *

     * This is identical to
     * SeekableXZInputStream(SeekableInputStream) except that
     * this also takes the arrayCache argument.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SeekableXZInputStream(SeekableInputStream in, ArrayCache arrayCache)
            throws IOException {
        this(in, -1, arrayCache);
    }

    /**
     * Creates a new seekable XZ decomporessor with an optional
     * memory usage limit.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      MemoryLimitException
     *                          decoded XZ Indexes would need more memory
     *                          than allowed by the memory usage limit
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     */
    public SeekableXZInputStream(SeekableInputStream in, int memoryLimit)
            throws IOException {
        this(in, memoryLimit, true);
    }

    /**
     * Creates a new seekable XZ decomporessor with an optional
     * memory usage limit.
     *

     * This is identical to
     * SeekableXZInputStream(SeekableInputStream,int)
     * except that this also takes the arrayCache argument.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      MemoryLimitException
     *                          decoded XZ Indexes would need more memory
     *                          than allowed by the memory usage limit
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SeekableXZInputStream(SeekableInputStream in, int memoryLimit,
                                 ArrayCache arrayCache)
            throws IOException {
        this(in, memoryLimit, true, arrayCache);
    }

    /**
     * Creates a new seekable XZ decomporessor with an optional
     * memory usage limit and ability to disable verification
     * of integrity checks.
     *

     * Note that integrity check verification should almost never be disabled.
     * Possible reasons to disable integrity check verification:
     *

     *
Trying to recover data from a corrupt .xz file.

     *
Speeding up decompression. This matters mostly with SHA-256
     *   or with files that have compressed extremely well. It's recommended
     *   that integrity checking isn't disabled for performance reasons
     *   unless the file integrity is verified externally in some other
     *   way.

     *

     *

     * verifyCheck only affects the integrity check of
     * the actual compressed data. The CRC32 fields in the headers
     * are always verified.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       verifyCheck if true, the integrity checks
     *                          will be verified; this should almost never
     *                          be set to false
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      MemoryLimitException
     *                          decoded XZ Indexes would need more memory
     *                          than allowed by the memory usage limit
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.6
     */
    public SeekableXZInputStream(SeekableInputStream in, int memoryLimit,
                                 boolean verifyCheck)
            throws IOException {
        this(in, memoryLimit, verifyCheck, ArrayCache.getDefaultCache());
    }

    /**
     * Creates a new seekable XZ decomporessor with an optional
     * memory usage limit and ability to disable verification
     * of integrity checks.
     *

     * This is identical to
     * SeekableXZInputStream(SeekableInputStream,int,boolean)
     * except that this also takes the arrayCache argument.
     *
     * @param       in          seekable input stream containing one or more
     *                          XZ Streams; the whole input stream is used
     *
     * @param       memoryLimit memory usage limit in kibibytes (KiB)
     *                          or -1 to impose no
     *                          memory usage limit
     *
     * @param       verifyCheck if true, the integrity checks
     *                          will be verified; this should almost never
     *                          be set to false
     *
     * @param       arrayCache  cache to be used for allocating large arrays
     *
     * @throws      XZFormatException
     *                          input is not in the XZ format
     *
     * @throws      CorruptedInputException
     *                          XZ data is corrupt or truncated
     *
     * @throws      UnsupportedOptionsException
     *                          XZ headers seem valid but they specify
     *                          options not supported by this implementation
     *
     * @throws      MemoryLimitException
     *                          decoded XZ Indexes would need more memory
     *                          than allowed by the memory usage limit
     *
     * @throws      EOFException
     *                          less than 6 bytes of input was available
     *                          from in, or (unlikely) the size
     *                          of the underlying stream got smaller while
     *                          this was reading from it
     *
     * @throws      IOException may be thrown by in
     *
     * @since 1.7
     */
    public SeekableXZInputStream(SeekableInputStream in, int memoryLimit,
                                 boolean verifyCheck, ArrayCache arrayCache)
            throws IOException {
        this.arrayCache = arrayCache;
        this.verifyCheck = verifyCheck;
        this.in = in;
        DataInputStream inData = new DataInputStream(in);

        // Check the magic bytes in the beginning of the file.
        {
            in.seek(0);
            byte[] buf = new byte[XZ.HEADER_MAGIC.length];
            inData.readFully(buf);
            if (!Arrays.equals(buf, XZ.HEADER_MAGIC))
                throw new XZFormatException();
        }

        // Get the file size and verify that it is a multiple of 4 bytes.
        long pos = in.length();
        if ((pos & 3) != 0)
            throw new CorruptedInputException(
                    "XZ file size is not a multiple of 4 bytes");

        // Parse the headers starting from the end of the file.
        byte[] buf = new byte[DecoderUtil.STREAM_HEADER_SIZE];
        long streamPadding = 0;

        while (pos > 0) {
            if (pos < DecoderUtil.STREAM_HEADER_SIZE)
                throw new CorruptedInputException();

            // Read the potential Stream Footer.
            in.seek(pos - DecoderUtil.STREAM_HEADER_SIZE);
            inData.readFully(buf);

            // Skip Stream Padding four bytes at a time.
            // Skipping more at once would be faster,
            // but usually there isn't much Stream Padding.
            if (buf[8] == 0x00 && buf[9] == 0x00 && buf[10] == 0x00
                    && buf[11] == 0x00) {
                streamPadding += 4;
                pos -= 4;
                continue;
            }

            // It's not Stream Padding. Update pos.
            pos -= DecoderUtil.STREAM_HEADER_SIZE;

            // Decode the Stream Footer and check if Backward Size
            // looks reasonable.
            StreamFlags streamFooter = DecoderUtil.decodeStreamFooter(buf);
            if (streamFooter.backwardSize >= pos)
                throw new CorruptedInputException(
                        "Backward Size in XZ Stream Footer is too big");

            // Check that the Check ID is supported. Store it in case this
            // is the first Stream in the file.
            check = Check.getInstance(streamFooter.checkType);

            // Remember which Check IDs have been seen.
            checkTypes |= 1 << streamFooter.checkType;

            // Seek to the beginning of the Index.
            in.seek(pos - streamFooter.backwardSize);

            // Decode the Index field.
            IndexDecoder index;
            try {
                index = new IndexDecoder(in, streamFooter, streamPadding,
                                         memoryLimit);
            } catch (MemoryLimitException e) {
                // IndexDecoder doesn't know how much memory we had
                // already needed so we need to recreate the exception.
                assert memoryLimit >= 0;
                throw new MemoryLimitException(
                        e.getMemoryNeeded() + indexMemoryUsage,
                        memoryLimit + indexMemoryUsage);
            }

            // Update the memory usage and limit counters.
            indexMemoryUsage += index.getMemoryUsage();
            if (memoryLimit >= 0) {
                memoryLimit -= index.getMemoryUsage();
                assert memoryLimit >= 0;
            }

            // Remember the uncompressed size of the largest Block.
            if (largestBlockSize < index.getLargestBlockSize())
                largestBlockSize = index.getLargestBlockSize();

            // Calculate the offset to the beginning of this XZ Stream and
            // check that it looks sane.
            long off = index.getStreamSize() - DecoderUtil.STREAM_HEADER_SIZE;
            if (pos < off)
                throw new CorruptedInputException("XZ Index indicates "
                        + "too big compressed size for the XZ Stream");

            // Seek to the beginning of this Stream.
            pos -= off;
            in.seek(pos);

            // Decode the Stream Header.
            inData.readFully(buf);
            StreamFlags streamHeader = DecoderUtil.decodeStreamHeader(buf);

            // Verify that the Stream Header matches the Stream Footer.
            if (!DecoderUtil.areStreamFlagsEqual(streamHeader, streamFooter))
                throw new CorruptedInputException(
                        "XZ Stream Footer does not match Stream Header");

            // Update the total uncompressed size of the file and check that
            // it doesn't overflow.
            uncompressedSize += index.getUncompressedSize();
            if (uncompressedSize < 0)
                throw new UnsupportedOptionsException("XZ file is too big");

            // Update the Block count and check that it fits into an int.
            blockCount += index.getRecordCount();
            if (blockCount < 0)
                throw new UnsupportedOptionsException(
                        "XZ file has over " + Integer.MAX_VALUE + " Blocks");

            // Add this Stream to the list of Streams.
            streams.add(index);

            // Reset to be ready to parse the next Stream.
            streamPadding = 0;
        }

        assert pos == 0;

        // Save it now that indexMemoryUsage has been substracted from it.
        this.memoryLimit = memoryLimit;

        // Store the relative offsets of the Streams. This way we don't
        // need to recalculate them in this class when seeking; the
        // IndexDecoder instances will handle them.
        IndexDecoder prev = streams.get(streams.size() - 1);
        for (int i = streams.size() - 2; i >= 0; --i) {
            IndexDecoder cur = streams.get(i);
            cur.setOffsets(prev);
            prev = cur;
        }

        // Initialize curBlockInfo to point to the first Stream.
        // The blockNumber will be left to -1 so that .hasNext()
        // and .setNext() work to get the first Block when starting
        // to decompress from the beginning of the file.
        IndexDecoder first = streams.get(streams.size() - 1);
        curBlockInfo = new BlockInfo(first);

        // queriedBlockInfo needs to be allocated too. The Stream used for
        // initialization doesn't matter though.
        queriedBlockInfo = new BlockInfo(first);
    }

    /**
     * Gets the types of integrity checks used in the .xz file.
     * Multiple checks are possible only if there are multiple
     * concatenated XZ Streams.
     *

     * The returned value has a bit set for every check type that is present.
     * For example, if CRC64 and SHA-256 were used, the return value is
     * (1 << XZ.CHECK_CRC64)
     * | (1 << XZ.CHECK_SHA256).
     */
    public int getCheckTypes() {
        return checkTypes;
    }

    /**
     * Gets the amount of memory in kibibytes (KiB) used by
     * the data structures needed to locate the XZ Blocks.
     * This is usually useless information but since it is calculated
     * for memory usage limit anyway, it is nice to make it available to too.
     */
    public int getIndexMemoryUsage() {
        return indexMemoryUsage;
    }

    /**
     * Gets the uncompressed size of the largest XZ Block in bytes.
     * This can be useful if you want to check that the file doesn't
     * have huge XZ Blocks which could make seeking to arbitrary offsets
     * very slow. Note that huge Blocks don't automatically mean that
     * seeking would be slow, for example, seeking to the beginning of
     * any Block is always fast.
     */
    public long getLargestBlockSize() {
        return largestBlockSize;
    }

    /**
     * Gets the number of Streams in the .xz file.
     *
     * @since 1.3
     */
    public int getStreamCount() {
        return streams.size();
    }

    /**
     * Gets the number of Blocks in the .xz file.
     *
     * @since 1.3
     */
    public int getBlockCount() {
        return blockCount;
    }

    /**
     * Gets the uncompressed start position of the given Block.
     *
     * @throws  IndexOutOfBoundsException if
     *          blockNumber < 0 or
     *          blockNumber >= getBlockCount().
     *
     * @since 1.3
     */
    public long getBlockPos(int blockNumber) {
        locateBlockByNumber(queriedBlockInfo, blockNumber);
        return queriedBlockInfo.uncompressedOffset;
    }

    /**
     * Gets the uncompressed size of the given Block.
     *
     * @throws  IndexOutOfBoundsException if
     *          blockNumber < 0 or
     *          blockNumber >= getBlockCount().
     *
     * @since 1.3
     */
    public long getBlockSize(int blockNumber) {
        locateBlockByNumber(queriedBlockInfo, blockNumber);
        return queriedBlockInfo.uncompressedSize;
    }

    /**
     * Gets the position where the given compressed Block starts in
     * the underlying .xz file.
     * This information is rarely useful to the users of this class.
     *
     * @throws  IndexOutOfBoundsException if
     *          blockNumber < 0 or
     *          blockNumber >= getBlockCount().
     *
     * @since 1.3
     */
    public long getBlockCompPos(int blockNumber) {
        locateBlockByNumber(queriedBlockInfo, blockNumber);
        return queriedBlockInfo.compressedOffset;
    }

    /**
     * Gets the compressed size of the given Block.
     * This together with the uncompressed size can be used to calculate
     * the compression ratio of the specific Block.
     *
     * @throws  IndexOutOfBoundsException if
     *          blockNumber < 0 or
     *          blockNumber >= getBlockCount().
     *
     * @since 1.3
     */
    public long getBlockCompSize(int blockNumber) {
        locateBlockByNumber(queriedBlockInfo, blockNumber);
        return (queriedBlockInfo.unpaddedSize + 3) & ~3;
    }

    /**
     * Gets integrity check type (Check ID) of the given Block.
     *
     * @throws  IndexOutOfBoundsException if
     *          blockNumber < 0 or
     *          blockNumber >= getBlockCount().
     *
     * @see #getCheckTypes()
     *
     * @since 1.3
     */
    public int getBlockCheckType(int blockNumber) {
        locateBlockByNumber(queriedBlockInfo, blockNumber);
        return queriedBlockInfo.getCheckType();
    }

    /**
     * Gets the number of the Block that contains the byte at the given
     * uncompressed position.
     *
     * @throws  IndexOutOfBoundsException if
     *          pos < 0 or
     *          pos >= length().
     *
     * @since 1.3
     */
    public int getBlockNumber(long pos) {
        locateBlockByPos(queriedBlockInfo, pos);
        return queriedBlockInfo.blockNumber;
    }

    /**
     * Decompresses the next byte from this input stream.
     *
     * @return      the next decompressed byte, or -1
     *              to indicate the end of the compressed stream
     *
     * @throws      CorruptedInputException
     * @throws      UnsupportedOptionsException
     * @throws      MemoryLimitException
     *
     * @throws      XZIOException if the stream has been closed
     *
     * @throws      IOException may be thrown by in
     */
    public int read() throws IOException {
        return read(tempBuf, 0, 1) == -1 ? -1 : (tempBuf[0] & 0xFF);
    }

    /**
     * Decompresses into an array of bytes.
     *

     * If len is zero, no bytes are read and 0
     * is returned. Otherwise this will try to decompress len
     * bytes of uncompressed data. Less than len bytes may
     * be read only in the following situations:
     *

     *
The end of the compressed data was reached successfully.

     *
An error is detected after at least one but less than
     *       len bytes have already been successfully
     *       decompressed. The next call with non-zero len
     *       will immediately throw the pending exception.

     *
An exception is thrown.

     *

     *
     * @param       buf         target buffer for uncompressed data
     * @param       off         start offset in buf
     * @param       len         maximum number of uncompressed bytes to read
     *
     * @return      number of bytes read, or -1 to indicate
     *              the end of the compressed stream
     *
     * @throws      CorruptedInputException
     * @throws      UnsupportedOptionsException
     * @throws      MemoryLimitException
     *
     * @throws      XZIOException if the stream has been closed
     *
     * @throws      IOException may be thrown by in
     */
    public int read(byte[] buf, int off, int len) throws IOException {
        if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length)
            throw new IndexOutOfBoundsException();

        if (len == 0)
            return 0;

        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        int size = 0;

        try {
            if (seekNeeded)
                seek();

            if (endReached)
                return -1;

            while (len > 0) {
                if (blockDecoder == null) {
                    seek();
                    if (endReached)
                        break;
                }

                int ret = blockDecoder.read(buf, off, len);

                if (ret > 0) {
                    curPos += ret;
                    size += ret;
                    off += ret;
                    len -= ret;
                } else if (ret == -1) {
                    blockDecoder = null;
                }
            }
        } catch (IOException e) {
            // We know that the file isn't simply truncated because we could
            // parse the Indexes in the constructor. So convert EOFException
            // to CorruptedInputException.
            if (e instanceof EOFException)
                e = new CorruptedInputException();

            exception = e;
            if (size == 0)
                throw e;
        }

        return size;
    }

    /**
     * Returns the number of uncompressed bytes that can be read
     * without blocking. The value is returned with an assumption
     * that the compressed input data will be valid. If the compressed
     * data is corrupt, CorruptedInputException may get
     * thrown before the number of bytes claimed to be available have
     * been read from this input stream.
     *
     * @return      the number of uncompressed bytes that can be read
     *              without blocking
     */
    public int available() throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        if (exception != null)
            throw exception;

        if (endReached || seekNeeded || blockDecoder == null)
            return 0;

        return blockDecoder.available();
    }

    /**
     * Closes the stream and calls in.close().
     * If the stream was already closed, this does nothing.
     *

     * This is equivalent to close(true).
     *
     * @throws  IOException if thrown by in.close()
     */
    public void close() throws IOException {
        close(true);
    }

    /**
     * Closes the stream and optionally calls in.close().
     * If the stream was already closed, this does nothing.
     * If close(false) has been called, a further
     * call of close(true) does nothing (it doesn't call
     * in.close()).
     *

     * If you don't want to close the underlying InputStream,
     * there is usually no need to worry about closing this stream either;
     * it's fine to do nothing and let the garbage collector handle it.
     * However, if you are using {@link ArrayCache}, close(false)
     * can be useful to put the allocated arrays back to the cache without
     * closing the underlying InputStream.
     *

     * Note that if you successfully reach the end of the stream
     * (read returns -1), the arrays are
     * automatically put back to the cache by that read call. In
     * this situation close(false) is redundant (but harmless).
     *
     * @throws  IOException if thrown by in.close()
     *
     * @since 1.7
     */
    public void close(boolean closeInput) throws IOException {
        if (in != null) {
            if (blockDecoder != null) {
                blockDecoder.close();
                blockDecoder = null;
            }

            try {
                if (closeInput)
                    in.close();
            } finally {
                in = null;
            }
        }
    }

    /**
     * Gets the uncompressed size of this input stream. If there are multiple
     * XZ Streams, the total uncompressed size of all XZ Streams is returned.
     */
    public long length() {
        return uncompressedSize;
    }

    /**
     * Gets the current uncompressed position in this input stream.
     *
     * @throws      XZIOException if the stream has been closed
     */
    public long position() throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        return seekNeeded ? seekPos : curPos;
    }

    /**
     * Seeks to the specified absolute uncompressed position in the stream.
     * This only stores the new position, so this function itself is always
     * very fast. The actual seek is done when read is called
     * to read at least one byte.
     *

     * Seeking past the end of the stream is possible. In that case
     * read will return -1 to indicate
     * the end of the stream.
     *
     * @param       pos         new uncompressed read position
     *
     * @throws      XZIOException
     *                          if pos is negative, or
     *                          if stream has been closed
     */
    public void seek(long pos) throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        if (pos < 0)
            throw new XZIOException("Negative seek position: " + pos);

        seekPos = pos;
        seekNeeded = true;
    }

    /**
     * Seeks to the beginning of the given XZ Block.
     *
     * @throws      XZIOException
     *              if blockNumber < 0 or
     *              blockNumber >= getBlockCount(),
     *              or if stream has been closed
     *
     * @since 1.3
     */
    public void seekToBlock(int blockNumber) throws IOException {
        if (in == null)
            throw new XZIOException("Stream closed");

        if (blockNumber < 0 || blockNumber >= blockCount)
            throw new XZIOException("Invalid XZ Block number: " + blockNumber);

        // This is a bit silly implementation. Here we locate the uncompressed
        // offset of the specified Block, then when doing the actual seek in
        // seek(), we need to find the Block number based on seekPos.
        seekPos = getBlockPos(blockNumber);
        seekNeeded = true;
    }

    /**
     * Does the actual seeking. This is also called when read
     * needs a new Block to decode.
     */
    private void seek() throws IOException {
        // If seek(long) wasn't called, we simply need to get the next Block
        // from the same Stream. If there are no more Blocks in this Stream,
        // then we behave as if seek(long) had been called.
        if (!seekNeeded) {
            if (curBlockInfo.hasNext()) {
                curBlockInfo.setNext();
                initBlockDecoder();
                return;
            }

            seekPos = curPos;
        }

        seekNeeded = false;

        // Check if we are seeking to or past the end of the file.
        if (seekPos >= uncompressedSize) {
            curPos = seekPos;

            if (blockDecoder != null) {
                blockDecoder.close();
                blockDecoder = null;
            }

            endReached = true;
            return;
        }

        endReached = false;

        // Locate the Block that contains the uncompressed target position.
        locateBlockByPos(curBlockInfo, seekPos);

        // Seek in the underlying stream and create a new Block decoder
        // only if really needed. We can skip it if the current position
        // is already in the correct Block and the target position hasn't
        // been decompressed yet.
        //
        // NOTE: If curPos points to the beginning of this Block, it's
        // because it was left there after decompressing an earlier Block.
        // In that case, decoding of the current Block hasn't been started
        // yet. (Decoding of a Block won't be started until at least one
        // byte will also be read from it.)
        if (!(curPos > curBlockInfo.uncompressedOffset && curPos <= seekPos)) {
            // Seek to the beginning of the Block.
            in.seek(curBlockInfo.compressedOffset);

            // Since it is possible that this Block is from a different
            // Stream than the previous Block, initialize a new Check.
            check = Check.getInstance(curBlockInfo.getCheckType());

            // Create a new Block decoder.
            initBlockDecoder();
            curPos = curBlockInfo.uncompressedOffset;
        }

        // If the target wasn't at a Block boundary, decompress and throw
        // away data to reach the target position.
        if (seekPos > curPos) {
            // NOTE: The "if" below is there just in case. In this situation,
            // blockDecoder.skip will always skip the requested amount
            // or throw an exception.
            long skipAmount = seekPos - curPos;
            if (blockDecoder.skip(skipAmount) != skipAmount)
                throw new CorruptedInputException();

            curPos = seekPos;
        }
    }

    /**
     * Locates the Block that contains the given uncompressed position.
     */
    private void locateBlockByPos(BlockInfo info, long pos) {
        if (pos < 0 || pos >= uncompressedSize)
            throw new IndexOutOfBoundsException(
                    "Invalid uncompressed position: " + pos);

        // Locate the Stream that contains the target position.
        IndexDecoder index;
        for (int i = 0; ; ++i) {
            index = streams.get(i);
            if (index.hasUncompressedOffset(pos))
                break;
        }

        // Locate the Block from the Stream that contains the target position.
        index.locateBlock(info, pos);

        assert (info.compressedOffset & 3) == 0;
        assert info.uncompressedSize > 0;
        assert pos >= info.uncompressedOffset;
        assert pos < info.uncompressedOffset + info.uncompressedSize;
    }

    /**
     * Locates the given Block and stores information about it
     * to info.
     */
    private void locateBlockByNumber(BlockInfo info, int blockNumber) {
        // Validate.
        if (blockNumber < 0 || blockNumber >= blockCount)
            throw new IndexOutOfBoundsException(
                    "Invalid XZ Block number: " + blockNumber);

        // Skip the search if info already points to the correct Block.
        if (info.blockNumber == blockNumber)
            return;

        // Search the Stream that contains the given Block and then
        // search the Block from that Stream.
        for (int i = 0; ; ++i) {
            IndexDecoder index = streams.get(i);
            if (index.hasRecord(blockNumber)) {
                index.setBlockInfo(info, blockNumber);
                return;
            }
        }
    }

    /**
     * Initializes a new BlockInputStream. This is a helper function for
     * seek().
     */
    private void initBlockDecoder() throws IOException {
        try {
            // Set it to null first so that GC can collect it if memory
            // runs tight when initializing a new BlockInputStream.
            if (blockDecoder != null) {
                blockDecoder.close();
                blockDecoder = null;
            }

            blockDecoder = new BlockInputStream(
                    in, check, verifyCheck, memoryLimit,
                    curBlockInfo.unpaddedSize, curBlockInfo.uncompressedSize,
                    arrayCache);
        } catch (MemoryLimitException e) {
            // BlockInputStream doesn't know how much memory we had
            // already needed so we need to recreate the exception.
            assert memoryLimit >= 0;
            throw new MemoryLimitException(
                    e.getMemoryNeeded() + indexMemoryUsage,
                    memoryLimit + indexMemoryUsage);
        } catch (IndexIndicatorException e) {
            // It cannot be Index so the file must be corrupt.
            throw new CorruptedInputException();
        }
    }
}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * 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.
 */

package org.apache.jasper.tagplugins.jstl;

import org.apache.jasper.compiler.tagplugin.*;

public final class ForEach implements TagPlugin {

    private boolean hasVar, hasBegin, hasEnd, hasStep;

    public void doTag(TagPluginContext ctxt) {

	String index = null;

	boolean hasVarStatus = ctxt.isAttributeSpecified("varStatus");
	if (hasVarStatus) {
	    ctxt.dontUseTagPlugin();
	    return;
	}

	hasVar = ctxt.isAttributeSpecified("var");
	hasBegin = ctxt.isAttributeSpecified("begin");
	hasEnd = ctxt.isAttributeSpecified("end");
	hasStep = ctxt.isAttributeSpecified("step");

	boolean hasItems = ctxt.isAttributeSpecified("items");
	if (hasItems) {
	    doCollection(ctxt);
	    return;
	}

	// We must have a begin and end attributes
	index = ctxt.getTemporaryVariableName();
	ctxt.generateJavaSource("for (int " + index + " = ");
	ctxt.generateAttribute("begin");
	ctxt.generateJavaSource("; " + index + " <= ");
	ctxt.generateAttribute("end");
	if (hasStep) {
	    ctxt.generateJavaSource("; " + index + "+=");
	    ctxt.generateAttribute("step");
	    ctxt.generateJavaSource(") {");
	}
	else {
	    ctxt.generateJavaSource("; " + index + "++) {");
	}

	// If var is specified and the body contains an EL, then sycn
	// the var attribute
	if (hasVar /* && ctxt.hasEL() */) {
	    ctxt.generateJavaSource("_jspx_page_context.setAttribute(");
	    ctxt.generateAttribute("var");
	    ctxt.generateJavaSource(", String.valueOf(" + index + "));");
	}
	ctxt.generateBody();
	ctxt.generateJavaSource("}");
    }

    /**
     * Generate codes for Collections
     * The pseudo code is:
     */
    private void doCollection(TagPluginContext ctxt) {

	ctxt.generateImport("java.util.*");
	generateIterators(ctxt);

        String itemsV = ctxt.getTemporaryVariableName();
        ctxt.generateJavaSource("Object " + itemsV + "= ");
        ctxt.generateAttribute("items");
        ctxt.generateJavaSource(";");

	String indexV=null, beginV=null, endV=null, stepV=null;
	if (hasBegin) {
	    beginV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("int " + beginV + " = ");
	    ctxt.generateAttribute("begin");
	    ctxt.generateJavaSource(";");
	}
	if (hasEnd) {
	    indexV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("int " + indexV + " = 0;");
	    endV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("int " + endV + " = ");
	    ctxt.generateAttribute("end");
	    ctxt.generateJavaSource(";");
	}
	if (hasStep) {
	    stepV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("int " + stepV + " = ");
	    ctxt.generateAttribute("step");
	    ctxt.generateJavaSource(";");
	}

        String iterV = ctxt.getTemporaryVariableName();
        ctxt.generateJavaSource("Iterator " + iterV + " = null;");
	// Object[]
	ctxt.generateJavaSource("if (" + itemsV + " instanceof Object[])");
	ctxt.generateJavaSource(iterV + "=toIterator((Object[])" + itemsV + ");");
	// boolean[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof boolean[])");
	ctxt.generateJavaSource(iterV + "=toIterator((boolean[])" + itemsV + ");");
	// byte[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof byte[])");
	ctxt.generateJavaSource(iterV + "=toIterator((byte[])" + itemsV + ");");
	// char[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof char[])");
	ctxt.generateJavaSource(iterV + "=toIterator((char[])" + itemsV + ");");
	// short[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof short[])");
	ctxt.generateJavaSource(iterV + "=toIterator((short[])" + itemsV + ");");
	// int[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof int[])");
	ctxt.generateJavaSource(iterV + "=toIterator((int[])" + itemsV + ");");
	// long[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof long[])");
	ctxt.generateJavaSource(iterV + "=toIterator((long[])" + itemsV + ");");
	// float[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof float[])");
	ctxt.generateJavaSource(iterV + "=toIterator((float[])" + itemsV + ");");
	// double[]
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof double[])");
	ctxt.generateJavaSource(iterV + "=toIterator((double[])" + itemsV + ");");

        // Collection
        ctxt.generateJavaSource("else if (" + itemsV + " instanceof Collection)");
        ctxt.generateJavaSource(iterV + "=((Collection)" + itemsV + ").iterator();");

        // Iterator
        ctxt.generateJavaSource("else if (" + itemsV + " instanceof Iterator)");
        ctxt.generateJavaSource(iterV + "=(Iterator)" + itemsV + ";");

	// Enumeration
	ctxt.generateJavaSource("else if (" + itemsV + " instanceof Enumeration)");
	ctxt.generateJavaSource(iterV + "=toIterator((Enumeration)" + itemsV + ");");

        // Map
        ctxt.generateJavaSource("else if (" + itemsV + " instanceof Map)");
        ctxt.generateJavaSource(iterV + "=((Map)" + itemsV + ").entrySet().iterator();");

	if (hasBegin) {
            String tV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("for (int " + tV + "=" + beginV + ";" +
			tV + ">0 && " + iterV + ".hasNext(); " +
			tV + "--)");
	    ctxt.generateJavaSource(iterV + ".next();");
	}

	ctxt.generateJavaSource("while (" + iterV + ".hasNext()){");
	if (hasVar) {
	    ctxt.generateJavaSource("_jspx_page_context.setAttribute(");
	    ctxt.generateAttribute("var");
	    ctxt.generateJavaSource(", " + iterV + ".next());");
	}

	ctxt.generateBody();

	if (hasStep) {
	    String tV = ctxt.getTemporaryVariableName();
	    ctxt.generateJavaSource("for (int " + tV + "=" + stepV + "-1;" +
			tV + ">0 && " + iterV + ".hasNext(); " +
			tV + "--)");
	    ctxt.generateJavaSource(iterV + ".next();");
	}
	if (hasEnd) {
	    if (hasStep) {
		ctxt.generateJavaSource(indexV + "+=" + stepV + ";");
	    }
	    else {
		ctxt.generateJavaSource(indexV + "++;");
	    }
	    if (hasBegin) {
		ctxt.generateJavaSource("if(" + beginV + "+" + indexV +
			">"+ endV + ")");
	    }
	    else {
		ctxt.generateJavaSource("if(" + indexV + ">" + endV + ")");
	    }
	    ctxt.generateJavaSource("break;");
	}
	ctxt.generateJavaSource("}");	// while
    }

    /**
     * Generate iterators for data types supported in items
     */
    private void generateIterators(TagPluginContext ctxt) {

	// Object[]
	ctxt.generateDeclaration("ObjectArrayIterator",
	    "private Iterator toIterator(final Object[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return a[index++];}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// boolean[]
	ctxt.generateDeclaration("booleanArrayIterator",
	    "private Iterator toIterator(final boolean[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Boolean(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// byte[]
	ctxt.generateDeclaration("byteArrayIterator",
	    "private Iterator toIterator(final byte[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Byte(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// char[]
	ctxt.generateDeclaration("charArrayIterator",
	    "private Iterator toIterator(final char[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Character(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// short[]
	ctxt.generateDeclaration("shortArrayIterator",
	    "private Iterator toIterator(final short[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Short(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// int[]
	ctxt.generateDeclaration("intArrayIterator",
	    "private Iterator toIterator(final int[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Integer(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// long[]
	ctxt.generateDeclaration("longArrayIterator",
	    "private Iterator toIterator(final long[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Long(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// float[]
	ctxt.generateDeclaration("floatArrayIterator",
	    "private Iterator toIterator(final float[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Float(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// double[]
	ctxt.generateDeclaration("doubleArrayIterator",
	    "private Iterator toIterator(final double[] a){\n" +
	    "  return (new Iterator() {\n" +
	    "    int index=0;\n" +
	    "    public boolean hasNext() {\n" +
	    "      return index < a.length;}\n" +
	    "    public Object next() {\n" +
	    "      return new Double(a[index++]);}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

	// Enumeration
	ctxt.generateDeclaration("enumIterator",
	    "private Iterator toIterator(final Enumeration e){\n" +
	    "  return (new Iterator() {\n" +
	    "    public boolean hasNext() {\n" +
	    "      return e.hasMoreElements();}\n" +
	    "    public Object next() {\n" +
	    "      return e.nextElement();}\n" +
	    "    public void remove() {}\n" +
	    "  });\n" +
	    "}"
	);

    }
}


/*******************************************************************************
 * Copyright (c) 2015, 2016 IBH SYSTEMS GmbH. and others
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *     Bachmann electronic GmbH - Bug #87 Unzip repository adapter always returns zero length file
 *******************************************************************************/
package org.eclipse.packagedrone.repo.adapter.unzip;

import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.activation.FileTypeMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.maven.artifact.versioning.ComparableVersion;
import org.eclipse.packagedrone.repo.MetaKey;
import org.eclipse.packagedrone.repo.channel.ArtifactInformation;
import org.eclipse.packagedrone.repo.channel.ChannelArtifactInformation;
import org.eclipse.packagedrone.repo.channel.ChannelId;
import org.eclipse.packagedrone.repo.channel.ChannelNotFoundException;
import org.eclipse.packagedrone.repo.channel.ChannelService;
import org.eclipse.packagedrone.repo.channel.ChannelService.By;
import org.eclipse.packagedrone.repo.channel.ReadableChannel;
import org.eclipse.packagedrone.repo.channel.servlet.AbstractChannelServiceServlet;
import org.eclipse.packagedrone.repo.channel.util.DownloadHelper;
import org.eclipse.packagedrone.utils.io.IOConsumer;
import org.eclipse.scada.utils.str.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.io.ByteStreams;

public class UnzipServlet extends AbstractChannelServiceServlet
{

    private static final MetaKey MK_MIME_TYPE = new MetaKey ( "mime", "type" );

    private static final MetaKey MK_MVN_EXTENSION = new MetaKey ( "mvn", "extension" );

    private static final long serialVersionUID = 1L;

    private final static Logger logger = LoggerFactory.getLogger ( UnzipServlet.class );

    private static final MetaKey MK_GROUP_ID = new MetaKey ( "mvn", "groupId" );

    private static final MetaKey MK_ARTIFACT_ID = new MetaKey ( "mvn", "artifactId" );

    private static final MetaKey MK_CLASSIFIER = new MetaKey ( "mvn", "classifier" );

    private static final MetaKey MK_VERSION = new MetaKey ( "mvn", "version" );

    private static final MetaKey MK_SNAPSHOT_VERSION = new MetaKey ( "mvn", "snapshotVersion" );

    private FileTypeMap fileTypeMap;

    @Override
    public void init () throws ServletException
    {
        super.init ();
        this.fileTypeMap = FileTypeMap.getDefaultFileTypeMap ();
    }

    @Override
    protected void doGet ( final HttpServletRequest request, final HttpServletResponse response ) throws ServletException, IOException
    {
        String pathString = request.getPathInfo ();
        if ( pathString == null )
        {
            handleNotFound ( "", response );
            return;
        }

        if ( pathString.startsWith ( "/" ) )
        {
            pathString = pathString.substring ( 1 );
        }

        final String[] toks = pathString.split ( "/" );
        if ( toks.length < 1 )
        {
            handleNotFound ( request.getPathInfo (), response );
            return;
        }

        final LinkedList<String> path = new LinkedList<> ( Arrays.asList ( toks ) );

        try
        {
            final String type = path.pop ();
            switch ( type )
            {
                case "artifact":
                    handleArtifact ( request, response, path );
                    return;
                case "newest":
                    handleNewest ( request, response, path );
                    return;
                case "newestZip":
                    handleNewestZip ( request, response, path );
                    return;
                case "newestByName":
                    handleNewestByName ( request, response, path );
                    return;
                case "maven":
                    handleMaven ( request, response, path );
                    return;
                default:
                    handleNotFoundError ( response, String.format ( "Unzip target type '%s' unknown.", type ) );
                    return;
            }
        }
        catch ( final IllegalStateException e )
        {
            response.setStatus ( HttpServletResponse.SC_NOT_FOUND );
            response.setContentType ( "text/plain" );
            response.getWriter ().write ( e.getMessage () );
            return;
        }
        catch ( final IllegalArgumentException e )
        {
            response.setStatus ( HttpServletResponse.SC_BAD_GATEWAY );
            response.setContentType ( "text/plain" );
            response.getWriter ().write ( e.getMessage () );
            return;
        }
    }

    protected void handleMaven ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
    {
        if ( path.isEmpty () )
        {
            throw new IllegalArgumentException ( String.format ( "The 'maven' type needs an addition type (latest, prefixed, ...)" ) );
        }

        final String mavenType = path.pop ();

        if ( path.isEmpty () )
        {
            throw new IllegalArgumentException ( String.format ( "The 'maven' type needs the channel id or name after the sub-type" ) );
        }

        final String channelIdOrName = path.pop ();

        try
        {
            getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {

                final IOConsumer<MavenVersionedArtifact> consumer = ( artifact ) -> {
                    streamArtifactEntry ( request, response, channel, artifact.getArtifact (), path );
                };

                switch ( mavenType )
                {
                    case "latest":
                        handleMavenLatest ( channel::getArtifacts, channel.getId (), path, false, consumer );
                        break;
                    case "latest-SNAPSHOT":
                        handleMavenLatest ( channel::getArtifacts, channel.getId (), path, true, consumer );
                        break;
                    case "prefixed":
                        handleMavenPrefixed ( channel::getArtifacts, channel.getId (), path, consumer );
                        break;
                    case "perfect":
                        handleMavenPerfect ( channel::getArtifacts, channel.getId (), path, consumer );
                        break;
                    default:
                        handleNotFoundError ( response, String.format ( "Unknown maven sub-type: %s", mavenType ) );
                        break;
                }

            } );
        }
        catch ( final ChannelNotFoundException e )
        {
            throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
        }
    }

    protected void handleNewest ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
    {
        handleWithFilter ( "newest", request, response, path, null );
    }

    protected void handleNewestZip ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
    {
        handleWithFilter ( "newestZip", request, response, path, UnzipServlet::isZip );
    }

    protected void handleWithFilter ( final String type, final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path, final Predicate<ArtifactInformation> filter ) throws IOException
    {
        requirePathPrefix ( path, 1, String.format ( "The '%1$s' method requires at least one parameter: channel. e.g. /unzip/%1$s/<channelIdOrName>/path/to/file", type ) );

        final String channelIdOrName = path.pop ();

        final ChannelService service = getService ( request );

        try
        {
            service.accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {
                List<ArtifactInformation> arts = new ArrayList<> ( channel.getArtifacts () );

                if ( filter != null )
                {
                    arts = arts.stream ().filter ( filter ).collect ( Collectors.toList () );
                }

                if ( arts.isEmpty () )
                {
                    throw new IllegalStateException ( String.format ( "Unable to find artifacts in channel '%s' (%s)", channelIdOrName, channel.getId () ) );
                }

                Collections.sort ( arts, Comparator.comparing ( ArtifactInformation::getCreationInstant ) );

                final ArtifactInformation artifact = arts.get ( 0 );

                logger.debug ( "Streaming artifact {} for channel {}", artifact.getId (), channelIdOrName );

                streamArtifactEntry ( request, response, channel, artifact, path );
            } );
        }
        catch ( final ChannelNotFoundException e )
        {
            throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
        }

    }

    protected static void processArtifacts ( final String sourceName, final List<MavenVersionedArtifact> arts, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
    {
        if ( arts.isEmpty () )
        {
            throw new IllegalStateException ( String.format ( "Unable to find artifacts in %s", sourceName ) );
        }

        Collections.sort ( arts ); // by version

        final MavenVersionedArtifact artifact = arts.get ( arts.size () - 1 ); // get last

        logger.debug ( "Streaming artifact {} for {}", artifact.getArtifact (), sourceName );

        consumer.accept ( artifact );
    }

    protected static void handleMavenPrefixed ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
    {
        requirePathPrefix ( path, 3, "The 'maven' method requires at least one parameter: channel. e.g. /unzip/maven/prefixed/<channelIdOrName>/<group.id>/<artifact.id>/<version>/path/to/file" );

        final String groupId = path.pop ();
        final String artifactId = path.pop ();
        final String versionString = path.pop ();

        final String versionPrefix;
        final int idx = versionString.toLowerCase ().indexOf ( 'x' );
        if ( idx > 0 )
        {
            // the x marks the spot
            versionPrefix = versionString.substring ( 0, idx - 1 );
        }
        else
        {
            versionPrefix = versionString;
        }

        final boolean snapshot = versionString.endsWith ( "-SNAPSHOT" );

        final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, snapshot, ( a ) -> a.toString ().startsWith ( versionPrefix ) );

        if ( arts.isEmpty () )
        {
            // no result,
            throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s, version: %s, snapshots: %s", groupId, artifactId, versionPrefix, snapshot ) );
        }

        processArtifacts ( String.format ( "maven artifact %s/%s/%s in channel %s", groupId, artifactId, versionString, channelId.getId () ), arts, consumer );
    }

    protected static void handleMavenPerfect ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
    {
        requirePathPrefix ( path, 3, "The 'maven' method requires at least one parameter: channel. e.g. /unzip/maven/perfect/<channelIdOrName>/<group.id>/<artifact.id>/<version>/path/to/file" );

        final String groupId = path.pop ();
        final String artifactId = path.pop ();
        final String versionString = path.pop ();

        final ComparableVersion v = new ComparableVersion ( versionString );

        final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, true, ( a ) -> a.compareTo ( v ) == 0 );

        if ( arts.isEmpty () )
        {
            // no result,
            throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s, version: %s", groupId, artifactId, v ) );
        }

        processArtifacts ( String.format ( "maven artifact %s/%s/%s in channel %s", groupId, artifactId, versionString, channelId.getId () ), arts, consumer );
    }

    protected static void handleMavenLatest ( final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final ChannelId channelId, final LinkedList<String> path, final boolean snapshot, final IOConsumer<MavenVersionedArtifact> consumer ) throws IOException
    {
        requirePathPrefix ( path, 2, "The 'maven' method requires at least two parameters: groupId, artifactId. e.g. /unzip/maven/latest(-SNAPSHOT)/<channelIdOrName>/<group.id>/<artifact.id>/path/to/file" );

        final String groupId = path.pop ();
        final String artifactId = path.pop ();

        final List<MavenVersionedArtifact> arts = getMavenArtifacts ( channelId.getId (), artifactsSupplier, groupId, artifactId, snapshot, null );

        if ( arts.isEmpty () )
        {
            // no result,
            throw new IllegalStateException ( String.format ( "No artifacts found for - groupId: %s, artifactId: %s", groupId, artifactId ) );
        }

        processArtifacts ( String.format ( "latest maven artifact %s/%s in channel %s", groupId, artifactId, channelId.getId () ), arts, consumer );
    }

    /**
     * Get a list of all relevant maven artifacts
     *
     * @param artifactsSupplier
     *            the supplier of artifacts
     * @param groupId
     *            the group id to filter for, must not be null
     * @param artifactId
     *            the artifact id to filter for, must not be null
     * @param snapshot
     *            whether to consider snapshot versions of not
     * @param versionFilter
     *            an optional version filter
     * @return a list of all matching artifacts wrapped in
     *         {@link MavenVersionedArtifact}, if there is a snapshot version
     *         present, then the snapshot version of used as version
     */
    protected static List<MavenVersionedArtifact> getMavenArtifacts ( final String channelId, final Supplier<Collection<ArtifactInformation>> artifactsSupplier, final String groupId, final String artifactId, final boolean snapshot, final Predicate<ComparableVersion> versionFilter )
    {
        final List<MavenVersionedArtifact> arts = new ArrayList<> ();

        for ( final ArtifactInformation ai : artifactsSupplier.get () )
        {
            if ( !isZip ( ai ) )
            {
                // if is is anot a zip, then this is not for the unzip plugin
                continue;
            }

            // fetch meta data

            final String mvnGroupId = ai.getMetaData ().get ( MK_GROUP_ID );
            final String mvnArtifactId = ai.getMetaData ().get ( MK_ARTIFACT_ID );

            final String classifier = ai.getMetaData ().get ( MK_CLASSIFIER );

            final String mvnVersion = ai.getMetaData ().get ( MK_VERSION );
            final String mvnSnapshotVersion = ai.getMetaData ().get ( MK_SNAPSHOT_VERSION );

            if ( mvnGroupId == null || mvnArtifactId == null || mvnVersion == null )
            {
                // no GAV information
                continue;
            }

            if ( classifier != null && !classifier.isEmpty () )
            {
                // no classifiers right now
                continue;
            }

            if ( !mvnGroupId.equals ( groupId ) || !mvnArtifactId.equals ( artifactId ) )
            {
                // wrong group or artifact id
                continue;
            }

            if ( !snapshot && ( mvnSnapshotVersion != null || mvnVersion.endsWith ( "-SNAPSHOT" ) ) )
            {
                // we are not looking for snapshots
                continue;
            }

            final ComparableVersion v = parseVersion ( mvnVersion );
            final ComparableVersion sv = parseVersion ( mvnSnapshotVersion );

            if ( v == null )
            {
                // unable to parse v
                continue;
            }

            if ( versionFilter == null )
            {
                // no filter, add it
                arts.add ( new MavenVersionedArtifact ( sv != null ? sv : v, channelId, ai ) );
            }
            else if ( versionFilter.test ( v ) )
            {
                // filter matched, add it
                arts.add ( new MavenVersionedArtifact ( sv != null ? sv : v, channelId, ai ) );
            }
            else if ( sv != null && versionFilter.test ( sv ) )
            {
                // we have a snapshot version and it matched, add it
                arts.add ( new MavenVersionedArtifact ( sv, channelId, ai ) );
            }
        }

        return arts;
    }

    private static ComparableVersion parseVersion ( final String version )
    {
        if ( version == null )
        {
            return null;
        }

        try
        {
            return new ComparableVersion ( version );
        }
        catch ( final Exception e )
        {
            logger.debug ( "Version not parsable: " + version, e );
            return null;
        }
    }

    /**
     * Check if an artifact is a ZIP file
     *

     * An artifact is a ZIP file if at least one of th following tests is true:
     *

     *
Its lower case name ends with .zip

     *
The meta data field mvn:extension is set to
     * zip
     *
The meta data field mime:type is set to
     * application/zip
     *

     *

     *
     * @param artifact
     *            the artifact to check
     * @return true if the artifact is a ZIP file,
     *         false otherwise
     */
    protected static boolean isZip ( final ArtifactInformation artifact )
    {
        if ( artifact.getName ().toLowerCase ().endsWith ( ".zip" ) )
        {
            return true;
        }

        final String mdExtension = artifact.getMetaData ().get ( MK_MVN_EXTENSION );
        if ( mdExtension != null && mdExtension.equalsIgnoreCase ( "zip" ) )
        {
            return true;
        }

        final String mdMime = artifact.getMetaData ().get ( MK_MIME_TYPE );
        if ( mdMime != null && mdMime.equalsIgnoreCase ( "application/zip" ) )
        {
            return true;
        }

        return false;
    }

    protected void handleNewestByName ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
    {
        requirePathPrefix ( path, 2, "The 'newestByName' method requires at least two parameters: channel and name. e.g. /unzip/newestByName/<channelIdOrName>/<artifactName>/path/to/file" );

        final String channelIdOrName = path.pop ();
        final String name = path.pop ();

        try
        {
            getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {

                final List<ArtifactInformation> arts = channel.getArtifacts ().stream ().filter ( ai -> ai.getName ().equals ( name ) ).collect ( toList () );

                if ( arts.isEmpty () )
                {
                    throw new IllegalStateException ( String.format ( "Unable to find artifact with name '%s' in channel '%s' (%s)", name, channelIdOrName, channel.getId () ) );
                }

                Collections.sort ( arts, Comparator.comparing ( ArtifactInformation::getCreationInstant ) );

                final ArtifactInformation artifact = arts.get ( 0 );

                logger.debug ( "Streaming artifact {} for name {} in channel {}", artifact.getId (), name, channelIdOrName );

                streamArtifactEntry ( request, response, channel, artifact, path );

            } );
        }
        catch ( final ChannelNotFoundException e )
        {
            throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
        }

    }

    private static void requirePathPrefix ( final LinkedList<String> path, final int pathPrefixCount, final String message )
    {
        if ( path.size () < pathPrefixCount )
        {
            throw new IllegalArgumentException ( message );
        }
    }

    protected void handleArtifact ( final HttpServletRequest request, final HttpServletResponse response, final LinkedList<String> path ) throws IOException
    {
        requirePathPrefix ( path, 2, "The 'artifact' method requires at least two parameters: channelId and artifactId. e.g. /unzip/artifact/<channelIdOrName>/<artifactId>/path/to/file" );

        final String channelIdOrName = path.pop ();

        try
        {
            getService ( request ).accessRun ( By.nameOrId ( channelIdOrName ), ReadableChannel.class, channel -> {

                final String artifactId = path.pop ();
                final Optional<ChannelArtifactInformation> artifact = channel.getArtifact ( artifactId );
                if ( !artifact.isPresent () )
                {
                    handleNotFoundError ( response, String.format ( "Artifact '%s' could not be found", artifactId ) );
                    return;
                }

                streamArtifactEntry ( request, response, channel, artifact.get (), path );

            } );
        }
        catch ( final ChannelNotFoundException e )
        {
            throw new IllegalStateException ( String.format ( "Channel with ID or name '%s' not found", channelIdOrName ) );
        }
    }

    protected void streamArtifactEntry ( final HttpServletRequest request, final HttpServletResponse response, final ReadableChannel channel, final ArtifactInformation artifact, final List<String> path ) throws IOException
    {
        final String localPath = StringHelper.join ( path, "/" );

        if ( localPath.isEmpty () )
        {
            DownloadHelper.streamArtifact ( response, artifact, Optional.empty (), true, channel, null );
            return;
        }

        // TODO: implement cache

        if ( !channel.getContext ().stream ( artifact.getId (), ( stream ) -> {
            try ( final ZipInputStream zis = new ZipInputStream ( stream ) )
            {
                ZipEntry entry;
                while ( ( entry = zis.getNextEntry () ) != null )
                {
                    if ( entry.getName ().equals ( localPath ) )
                    {
                        final String type = this.fileTypeMap.getContentType ( entry.getName () );
                        response.setContentType ( type );
                        final long size = entry.getSize ();
                        if ( size > 0 )
                        {
                            response.setContentLengthLong ( entry.getSize () );
                        }
                        response.setDateHeader ( "Last-Modified", artifact.getCreationTimestamp ().getTime () );
                        ByteStreams.copy ( zis, response.getOutputStream () );
                        break;
                    }
                }
                if ( entry == null || !entry.getName ().equals ( localPath ) )
                {
                    handleNotFoundError ( response, String.format ( "File entry '%s' could not be found in artifact '%s'", localPath, artifact.getId () ) );
                }
            }
        } ) )
        {
            handleNotFoundError ( response, String.format ( "Artifact %s could not be found", artifact.getId () ) );
        }
    }

    protected void handleNotFound ( final String path, final HttpServletResponse response ) throws IOException
    {
        handleNotFoundError ( response, String.format ( "Resource '%s' cound not be found", path ) );
    }

    protected void handleNotFoundError ( final HttpServletResponse response, final String message ) throws IOException
    {
        response.setStatus ( HttpServletResponse.SC_NOT_FOUND );
        response.setContentType ( "text/plain" );
        response.getWriter ().write ( message );
    }
}