    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.io.input;
    018
    019import static org.apache.commons.io.IOUtils.EOF;
    020
    021import java.io.BufferedInputStream;
    022import java.io.IOException;
    023import java.io.InputStream;
    024import java.nio.ByteBuffer;
    025import java.nio.channels.FileChannel;
    026import java.nio.channels.FileChannel.MapMode;
    027import java.nio.file.Path;
    028import java.nio.file.StandardOpenOption;
    029
    030import org.apache.commons.io.build.AbstractStreamBuilder;
    031
    032/**
    033 * An {@link InputStream} that utilizes memory mapped files to improve performance. A sliding window of the file is
    034 * mapped to memory to avoid mapping the entire file to memory at one time. The size of the sliding buffer is
    035 * configurable.
    036 * <p>
    037 * For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of
    038 * kilobytes of data. From the standpoint of performance. it is generally only worth mapping relatively large files into
    039 * memory.
    040 * </p>
    041 * <p>
    042 * Note: Use of this class does not necessarily obviate the need to use a {@link BufferedInputStream}. Depending on the
    043 * use case, the use of buffering may still further improve performance. For example:
    044 * </p>
    045 * <pre>
    046 * new BufferedInputStream(new GzipInputStream(new MemoryMappedFileInputStream(path))))
    047 * </pre>
    048 * <p>
    049 * should outperform:
    050 * </p>
    051 * <pre>
    052 * new GzipInputStream(new MemoryMappedFileInputStream(path))
    053 * </pre>
    054 *
    055 * @since 2.12.0
    056 */
    057public final class MemoryMappedFileInputStream extends InputStream {
    058
    059    /**
    060     * Builds a new {@link MemoryMappedFileInputStream} instance.
    061     * <p>
    062     * For example:
    063     * </p>
    064     * <pre>{@code
    065     * MemoryMappedFileInputStream s = MemoryMappedFileInputStream.builder()
    066     *   .setPath(path)
    067     *   .setBufferSize(256 * 1024)
    068     *   .get()}
    069     * </pre>
    070     * <p>
    071     * @since 2.12.0
    072     */
    073    public static class Builder extends AbstractStreamBuilder<MemoryMappedFileInputStream, Builder> {
    074
    075        public Builder() {
    076            setBufferSizeDefault(DEFAULT_BUFFER_SIZE);
    077            setBufferSize(DEFAULT_BUFFER_SIZE);
    078        }
    079
    080        /**
    081         * Constructs a new instance.
    082         *
    083         * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    084         */
    085        @Override
    086        public MemoryMappedFileInputStream get() throws IOException {
    087            return new MemoryMappedFileInputStream(getOrigin().getPath(), getBufferSize());
    088        }
    089    }
    090
    091    /**
    092     * Default size of the sliding memory mapped buffer. We use 256K, equal to 65536 pages (given a 4K page size).
    093     * Increasing the value beyond the default size will generally not provide any increase in throughput.
    094     */
    095    private static final int DEFAULT_BUFFER_SIZE = 256 * 1024;
    096
    097    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new byte[0]).asReadOnlyBuffer();
    098
    099    /**
    100     * Constructs a new {@link Builder}.
    101     *
    102     * @return a new {@link Builder}.
    103     * @since 2.12.0
    104     */
    105    public static Builder builder() {
    106        return new Builder();
    107    }
    108
    109    private final int bufferSize;
    110    private final FileChannel channel;
    111    private ByteBuffer buffer = EMPTY_BUFFER;
    112    private boolean closed;
    113
    114    /**
    115     * The starting position (within the file) of the next sliding buffer.
    116     */
    117    private long nextBufferPosition;
    118
    119    /**
    120     * Constructs a new instance.
    121     *
    122     * @param file The path of the file to open.
    123     * @param bufferSize Size of the sliding buffer.
    124     * @throws IOException If an I/O error occurs.
    125     */
    126    private MemoryMappedFileInputStream(final Path file, final int bufferSize) throws IOException {
    127        this.bufferSize = bufferSize;
    128        this.channel = FileChannel.open(file, StandardOpenOption.READ);
    129    }
    130
    131    @Override
    132    public int available() throws IOException {
    133        return buffer.remaining();
    134    }
    135
    136    private void cleanBuffer() {
    137        if (ByteBufferCleaner.isSupported() && buffer.isDirect()) {
    138            ByteBufferCleaner.clean(buffer);
    139        }
    140    }
    141
    142    @Override
    143    public void close() throws IOException {
    144        if (!closed) {
    145            cleanBuffer();
    146            buffer = null;
    147            channel.close();
    148            closed = true;
    149        }
    150    }
    151
    152    private void ensureOpen() throws IOException {
    153        if (closed) {
    154            throw new IOException("Stream closed");
    155        }
    156    }
    157
    158    int getBufferSize() {
    159        return bufferSize;
    160    }
    161
    162    private void nextBuffer() throws IOException {
    163        final long remainingInFile = channel.size() - nextBufferPosition;
    164        if (remainingInFile > 0) {
    165            final long amountToMap = Math.min(remainingInFile, bufferSize);
    166            cleanBuffer();
    167            buffer = channel.map(MapMode.READ_ONLY, nextBufferPosition, amountToMap);
    168            nextBufferPosition += amountToMap;
    169        } else {
    170            buffer = EMPTY_BUFFER;
    171        }
    172    }
    173
    174    @Override
    175    public int read() throws IOException {
    176        ensureOpen();
    177        if (!buffer.hasRemaining()) {
    178            nextBuffer();
    179            if (!buffer.hasRemaining()) {
    180                return EOF;
    181            }
    182        }
    183        return Short.toUnsignedInt(buffer.get());
    184    }
    185
    186    @Override
    187    public int read(final byte[] b, final int off, final int len) throws IOException {
    188        ensureOpen();
    189        if (!buffer.hasRemaining()) {
    190            nextBuffer();
    191            if (!buffer.hasRemaining()) {
    192                return EOF;
    193            }
    194        }
    195        final int numBytes = Math.min(buffer.remaining(), len);
    196        buffer.get(b, off, numBytes);
    197        return numBytes;
    198    }
    199
    200    @Override
    201    public long skip(final long n) throws IOException {
    202        ensureOpen();
    203        if (n <= 0) {
    204            return 0;
    205        }
    206        if (n <= buffer.remaining()) {
    207            buffer.position((int) (buffer.position() + n));
    208            return n;
    209        }
    210        final long remainingInFile = channel.size() - nextBufferPosition;
    211        final long skipped = buffer.remaining() + Math.min(remainingInFile, n - buffer.remaining());
    212        nextBufferPosition += skipped - buffer.remaining();
    213        nextBuffer();
    214        return skipped;
    215    }
    216
    217}