| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.pdfbox.io; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.file.StandardOpenOption; |
| import java.util.EnumSet; |
| import java.util.Optional; |
| import java.util.function.Consumer; |
| |
| /** |
| * An implementation of the RandomAccess interface backed by a memory mapped file channel. The whole file is mapped to |
| * memory and the max size is limited to Integer.MAX_VALUE. |
| */ |
| public class RandomAccessReadMemoryMappedFile implements RandomAccessRead |
| { |
| |
| // mapped byte buffer |
| private ByteBuffer mappedByteBuffer; |
| |
| // size of the whole file |
| private final long size; |
| |
| // file channel of the file to be read |
| private FileChannel fileChannel; |
| |
| // function to unmap the byte buffer |
| private Consumer<? super ByteBuffer> unmapper = IOUtils::unmap; |
| |
| /** |
| * Default constructor. |
| */ |
| public RandomAccessReadMemoryMappedFile(String filename) throws IOException |
| { |
| this(new File(filename)); |
| } |
| |
| /** |
| * Default constructor. |
| */ |
| public RandomAccessReadMemoryMappedFile(File file) throws IOException |
| { |
| fileChannel = FileChannel.open(file.toPath(), EnumSet.of(StandardOpenOption.READ)); |
| size = fileChannel.size(); |
| // TODO only ints are allowed -> implement paging |
| if (size > Integer.MAX_VALUE) |
| { |
| throw new IOException(getClass().getName() |
| + " doesn't yet support files bigger than " |
| + Integer.MAX_VALUE); |
| } |
| // map the whole file to memory |
| mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size); |
| } |
| |
| private RandomAccessReadMemoryMappedFile(RandomAccessReadMemoryMappedFile parent) |
| { |
| mappedByteBuffer = parent.mappedByteBuffer.duplicate(); |
| size = parent.size; |
| mappedByteBuffer.rewind(); |
| // unmap doesn't work on duplicate, see Unsafe#invokeCleaner |
| unmapper = null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void close() throws IOException |
| { |
| if (fileChannel != null) |
| { |
| fileChannel.close(); |
| } |
| Optional.ofNullable(unmapper).ifPresent(u -> u.accept(mappedByteBuffer)); |
| mappedByteBuffer = null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void seek(long position) throws IOException |
| { |
| checkClosed(); |
| if (position < 0) |
| { |
| throw new IOException("Invalid position "+position); |
| } |
| // it is allowed to jump beyond the end of the file |
| // jump to the end of the reader |
| mappedByteBuffer.position((int) Math.min(position, size)); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public long getPosition() throws IOException |
| { |
| checkClosed(); |
| return mappedByteBuffer.position(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int read() throws IOException |
| { |
| if (isEOF()) |
| { |
| return -1; |
| } |
| return mappedByteBuffer.get() & 0xff; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int read(byte[] b, int offset, int length) throws IOException |
| { |
| if (isEOF()) |
| { |
| return -1; |
| } |
| int remainingBytes = (int)size - mappedByteBuffer.position(); |
| mappedByteBuffer.get(b, offset, Math.min(remainingBytes, length)); |
| return Math.min(remainingBytes, length); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public long length() throws IOException |
| { |
| checkClosed(); |
| return size; |
| } |
| |
| /** |
| * Ensure that the RandomAccessReadMemoryMappedFile is not closed |
| * |
| * @throws IOException |
| */ |
| private void checkClosed() throws IOException |
| { |
| if (isClosed()) |
| { |
| throw new IOException(getClass().getSimpleName() + " already closed"); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isClosed() |
| { |
| return mappedByteBuffer == null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isEOF() throws IOException |
| { |
| checkClosed(); |
| return mappedByteBuffer.position() >= size; |
| } |
| |
| @Override |
| public RandomAccessReadView createView(long startPosition, long streamLength) |
| { |
| return new RandomAccessReadView(new RandomAccessReadMemoryMappedFile(this), startPosition, |
| streamLength, true); |
| } |
| } |