123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- /* ====================================================================
- 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.poi.poifs.nio;
-
- import java.io.Closeable;
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
- import java.nio.ByteBuffer;
- import java.nio.channels.Channels;
- import java.nio.channels.FileChannel;
- import java.nio.channels.WritableByteChannel;
- import java.util.IdentityHashMap;
-
- import org.apache.poi.util.IOUtils;
- import org.apache.poi.util.POILogFactory;
- import org.apache.poi.util.POILogger;
-
- /**
- * A POIFS {@link DataSource} backed by a File
- */
- public class FileBackedDataSource extends DataSource implements Closeable {
- private static final POILogger LOG = POILogFactory.getLogger(FileBackedDataSource.class);
-
- private final FileChannel channel;
- private Long channelSize;
-
- private final boolean writable;
- // remember file base, which needs to be closed too
- private final RandomAccessFile srcFile;
-
- // Buffers which map to a file-portion are not closed automatically when the Channel is closed
- // therefore we need to keep the list of mapped buffers and do some ugly reflection to try to
- // clean the buffer during close().
- // See https://bz.apache.org/bugzilla/show_bug.cgi?id=58480,
- // http://stackoverflow.com/questions/3602783/file-access-synchronized-on-java-object and
- // http://bugs.java.com/view_bug.do?bug_id=4724038 for related discussions
- // https://stackoverflow.com/questions/36077641/java-when-does-direct-buffer-released
- private final IdentityHashMap<ByteBuffer,ByteBuffer> buffersToClean = new IdentityHashMap<>();
-
- public FileBackedDataSource(File file) throws FileNotFoundException {
- this(newSrcFile(file, "r"), true);
- }
-
- public FileBackedDataSource(File file, boolean readOnly) throws FileNotFoundException {
- this(newSrcFile(file, readOnly ? "r" : "rw"), readOnly);
- }
-
- public FileBackedDataSource(RandomAccessFile srcFile, boolean readOnly) {
- this(srcFile, srcFile.getChannel(), readOnly);
- }
-
- public FileBackedDataSource(FileChannel channel, boolean readOnly) {
- this(null, channel, readOnly);
- }
-
- private FileBackedDataSource(RandomAccessFile srcFile, FileChannel channel, boolean readOnly) {
- this.srcFile = srcFile;
- this.channel = channel;
- this.writable = !readOnly;
- }
-
- public boolean isWriteable() {
- return this.writable;
- }
-
- public FileChannel getChannel() {
- return this.channel;
- }
-
- @Override
- public ByteBuffer read(int length, long position) throws IOException {
- if (position >= size()) {
- throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
- }
-
- // TODO Could we do the read-only case with MapMode.PRIVATE instead?
- // See https://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.MapMode.html#PRIVATE
- // Or should we have 3 modes instead of the current boolean -
- // read-write, read-only, read-to-write-elsewhere?
-
- // Do we read or map (for read/write)?
- ByteBuffer dst;
- if (writable) {
- dst = channel.map(FileChannel.MapMode.READ_WRITE, position, length);
-
- // remember this buffer for cleanup
- buffersToClean.put(dst,dst);
- } else {
- channel.position(position);
-
- // allocate the buffer on the heap if we cannot map the data in directly
- dst = ByteBuffer.allocate(length);
-
- // Read the contents and check that we could read some data
- int worked = IOUtils.readFully(channel, dst);
- if (worked == -1) {
- throw new IndexOutOfBoundsException("Position " + position + " past the end of the file");
- }
- }
-
- // make it ready for reading
- dst.position(0);
-
- // All done
- return dst;
- }
-
- @Override
- public void write(ByteBuffer src, long position) throws IOException {
- channel.write(src, position);
-
- // we have to re-read size if we write "after" the recorded one
- if(channelSize != null && position >= channelSize) {
- channelSize = null;
- }
- }
-
- @Override
- public void copyTo(OutputStream stream) throws IOException {
- // Wrap the OutputSteam as a channel
- try (WritableByteChannel out = Channels.newChannel(stream)) {
- // Now do the transfer
- channel.transferTo(0, channel.size(), out);
- }
- }
-
- @Override
- public long size() throws IOException {
- // this is called often and profiling showed that channel.size()
- // was taking a large part of processing-time, so we only read it
- // once
- if(channelSize == null) {
- channelSize = channel.size();
- }
- return channelSize;
- }
-
- public void releaseBuffer(ByteBuffer buffer) {
- ByteBuffer previous = buffersToClean.remove(buffer);
- if (previous != null) {
- unmap(previous);
- }
- }
-
- @Override
- public void close() throws IOException {
- // also ensure that all buffers are unmapped so we do not keep files locked on Windows
- // We consider it a bug if a Buffer is still in use now!
- buffersToClean.forEach((k,v) -> unmap(v));
- buffersToClean.clear();
-
- if (srcFile != null) {
- // see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4796385
- srcFile.close();
- } else {
- channel.close();
- }
- }
-
- private static RandomAccessFile newSrcFile(File file, String mode) throws FileNotFoundException {
- if (!file.exists()) {
- throw new FileNotFoundException(file.toString());
- }
- return new RandomAccessFile(file, mode);
- }
-
- // need to use reflection to avoid depending on the sun.nio internal API
- // unfortunately this might break silently with newer/other Java implementations,
- // but we at least have unit-tests which will indicate this when run on Windows
- private static void unmap(final ByteBuffer buffer) {
- // not necessary for HeapByteBuffer, avoid lots of log-output on this class
- if (buffer.getClass().getName().endsWith("HeapByteBuffer")) {
- return;
- }
-
- if (CleanerUtil.UNMAP_SUPPORTED) {
- try {
- CleanerUtil.getCleaner().freeBuffer(buffer);
- } catch (IOException e) {
- LOG.log(POILogger.WARN, "Failed to unmap the buffer", e);
- }
- } else {
- LOG.log(POILogger.DEBUG, CleanerUtil.UNMAP_NOT_SUPPORTED_REASON);
- }
- }
- }
|