class RecyclableInflater implements Resettable, AutoCloseable {
        /**
         * Create a new {@link Inflater} instance with the "nowrap" option (which is needed for zipfile entries).
         */
        private final Inflater inflater = new Inflater(/* nowrap = */ true);

        /**
         * Get the {@link Inflater} instance.
         *
         * @return the {@link Inflater} instance.
         */
        public Inflater getInflater() {
            return inflater;
        }

        /**
         * Called when an {@link Inflater} instance is recycled, to reset the inflater so it can accept new input.
         */
        @Override
        public void reset() {
            inflater.reset();
        }

        /** Called when the {@link Recycler} instance is closed, to destroy the {@link Inflater} instance. */
        @Override
        public void close() {
            inflater.end();
        }
    }

    /**
     * Wrap an {@link InputStream} with an {@link InflaterInputStream}, recycling the {@link Inflater} instance.
     *
     * @param rawInputStream
     *            the raw input stream
     * @return the inflater input stream
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    public InputStream openInflaterInputStream(final InputStream rawInputStream) throws IOException {<FILL_FUNCTION_BODY>

        return new InputStream() {
            // Gen Inflater instance with nowrap set to true (needed by zip entries)
            private final RecyclableInflater recyclableInflater = inflaterRecycler.acquire();
            private final Inflater inflater = recyclableInflater.getInflater();
            private final AtomicBoolean closed = new AtomicBoolean();
            private final byte[] buf = new byte[INFLATE_BUF_SIZE];
            private static final int INFLATE_BUF_SIZE = 8192;

            @Override
            public int read() throws IOException {
                if (closed.get()) {
                    throw new IOException("Already closed");
                } else if (inflater.finished()) {
                    return -1;
                }
                final int numDeflatedBytesRead = read(buf, 0, 1);
                if (numDeflatedBytesRead < 0) {
                    return -1;
                } else {
                    return buf[0] & 0xff;
                }
            }

            @Override
            public int read(final byte[] outBuf, final int off, final int len) throws IOException {
                if (closed.get()) {
                    throw new IOException("Already closed");
                } else if (len < 0) {
                    throw new IllegalArgumentException("len cannot be negative");
                } else if (len == 0) {
                    return 0;
                }
                try {
                    // Keep fetching data from rawInputStream until buffer is full or inflater has finished
                    int totInflatedBytes = 0;
                    while (!inflater.finished() && totInflatedBytes < len) {
                        final int numInflatedBytes = inflater.inflate(outBuf, off + totInflatedBytes,
                                len - totInflatedBytes);
                        if (numInflatedBytes == 0) {
                            if (inflater.needsDictionary()) {
                                // Should not happen for jarfiles
                                throw new IOException("Inflater needs preset dictionary");
                            } else if (inflater.needsInput()) {
                                // Read a chunk of data from the raw InputStream
                                final int numRawBytesRead = rawInputStream.read(buf, 0, buf.length);
                                if (numRawBytesRead == -1) {
                                    // An extra dummy byte is needed at the end of the input stream when
                                    // using the "nowrap" Inflater option.
                                    // See: ZipFile.ZipFileInflaterInputStream.fill()
                                    buf[0] = (byte) 0;
                                    inflater.setInput(buf, 0, 1);
                                } else {
                                    // Deflate the chunk of data
                                    inflater.setInput(buf, 0, numRawBytesRead);
                                }
                            }
                        } else {
                            totInflatedBytes += numInflatedBytes;
                        }
                    }
                    if (totInflatedBytes == 0) {
                        // If no bytes were inflated, return -1 as required by read() API contract
                        return -1;
                    }
                    return totInflatedBytes;

                } catch (final DataFormatException e) {
                    throw new ZipException(
                            e.getMessage() != null ? e.getMessage() : "Invalid deflated zip entry data");
                }
            }

            @Override
            public long skip(final long numToSkip) throws IOException {
                if (closed.get()) {
                    throw new IOException("Already closed");
                } else if (numToSkip < 0) {
                    throw new IllegalArgumentException("numToSkip cannot be negative");
                } else if (numToSkip == 0) {
                    return 0;
                } else if (inflater.finished()) {
                    return -1;
                }
                long totBytesSkipped = 0L;
                for (;;) {
                    final int readLen = (int) Math.min(numToSkip - totBytesSkipped, buf.length);
                    final int numBytesRead = read(buf, 0, readLen);
                    if (numBytesRead > 0) {
                        totBytesSkipped -= numBytesRead;
                    } else {
                        break;
                    }
                }
                return totBytesSkipped;
            }

            @Override
            public int available() throws IOException {
                if (closed.get()) {
                    throw new IOException("Already closed");
                }
                // We don't know how many bytes are available, but have to return greater than
                // zero if there is still input, according to the API contract. Hopefully nothing
                // relies on this and ends up reading just one byte at a time.
                return inflater.finished() ? 0 : 1;
            }

            @Override
            public synchronized void mark(final int readlimit) {
                throw new IllegalArgumentException("Not supported");
            }

            @Override
            public synchronized void reset() throws IOException {
                throw new IllegalArgumentException("Not supported");
            }

            @Override
            public boolean markSupported() {
                return false;
            }

            @Override
            public void close() {
                if (!closed.getAndSet(true)) {
                    try {
                        rawInputStream.close();
                    } catch (final Exception e) {
                        // Ignore
                    }
                    // Reset and recycle inflater instance
                    inflaterRecycler.recycle(recyclableInflater);
                }
            }
        };
    
<no_super_class>