org.apache.pdfbox.io.ScratchFileBuffer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pdfbox Show documentation
Show all versions of pdfbox Show documentation
The Apache PDFBox library is an open source Java tool for working with PDF documents.
/*
* 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.EOFException;
import java.io.IOException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSStream;
/**
* Implementation of {@link RandomAccess} as sequence of multiple fixed size pages handled
* by {@link ScratchFile}.
*/
class ScratchFileBuffer implements RandomAccess
{
private final int pageSize;
/**
* The underlying page handler.
*/
private ScratchFile pageHandler;
/**
* The number of bytes of content in this buffer.
*/
private long size = 0;
/**
* Index of current page in {@link #pageIndexes} (the nth page within this buffer).
*/
private int currentPagePositionInPageIndexes;
/**
* The offset of the current page within this buffer.
*/
private long currentPageOffset;
/**
* The current page data.
*/
private byte[] currentPage;
/**
* The current position (for next read/write) of the buffer as an offset in the current page.
*/
private int positionInPage;
/**
* true
if current page was changed by a write method
*/
private boolean currentPageContentChanged = false;
/** contains ordered list of pages with the index the page is known by page handler ({@link ScratchFile}) */
private int[] pageIndexes = new int[16];
/** number of pages held by this buffer */
private int pageCount = 0;
private static final Log LOG = LogFactory.getLog(ScratchFileBuffer.class);
/**
* Creates a new buffer using pages handled by provided {@link ScratchFile}.
*
* @param pageHandler The {@link ScratchFile} managing the pages to be used by this buffer.
*
* @throws IOException If getting first page failed.
*/
ScratchFileBuffer(ScratchFile pageHandler) throws IOException
{
pageHandler.checkClosed();
this.pageHandler = pageHandler;
pageSize = this.pageHandler.getPageSize();
addPage();
}
/**
* Checks if this buffer, or the underlying {@link ScratchFile} have been closed,
* throwing {@link IOException} if so.
*
* @throws IOException If either this buffer, or the underlying {@link ScratchFile} have been closed.
*/
private void checkClosed() throws IOException
{
if (pageHandler == null)
{
throw new IOException("Buffer already closed");
}
pageHandler.checkClosed();
}
/**
* Adds a new page and positions all pointers to start of new page.
*
* @throws IOException if requesting a new page fails
*/
private void addPage() throws IOException
{
if (pageCount+1 >= pageIndexes.length)
{
int newSize = pageIndexes.length*2;
// check overflow
if (newSizeIf this is not the case we go to next page (writing
* current one if changed). If current buffer has no more
* pages we add a new one.
*
* @param addNewPageIfNeeded if true
it is allowed to add a new page in case
* we are currently at end of last buffer page
*
* @return true
if we were successful positioning pointer before end of page;
* we might return false
if it is not allowed to add another page
* and current pointer points at end of last page
*
* @throws IOException
*/
private boolean ensureAvailableBytesInPage(boolean addNewPageIfNeeded) throws IOException
{
if (positionInPage >= pageSize)
{
// page full
if (currentPageContentChanged)
{
// write page
pageHandler.writePage(pageIndexes[currentPagePositionInPageIndexes], currentPage);
currentPageContentChanged = false;
}
// get new page
if (currentPagePositionInPageIndexes+1 < pageCount)
{
// we already have more pages assigned (there was a backward seek before)
currentPage = pageHandler.readPage(pageIndexes[++currentPagePositionInPageIndexes]);
currentPageOffset = ((long)currentPagePositionInPageIndexes) * pageSize;
positionInPage = 0;
}
else if (addNewPageIfNeeded)
{
// need new page
addPage();
}
else
{
// we are at last page and are not allowed to add new page
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void write(int b) throws IOException
{
checkClosed();
ensureAvailableBytesInPage(true);
currentPage[positionInPage++] = (byte) b;
currentPageContentChanged = true;
if(currentPageOffset + positionInPage > size)
{
size = currentPageOffset + positionInPage;
}
}
/**
* {@inheritDoc}
*/
@Override
public void write(byte[] b) throws IOException
{
write(b, 0, b.length);
}
/**
* {@inheritDoc}
*/
@Override
public void write(byte[] b, int off, int len) throws IOException
{
checkClosed();
int remain = len;
int bOff = off;
while (remain > 0)
{
ensureAvailableBytesInPage(true);
int bytesToWrite = Math.min(remain, pageSize-positionInPage);
System.arraycopy(b, bOff, currentPage, positionInPage, bytesToWrite);
positionInPage += bytesToWrite;
currentPageContentChanged = true;
bOff += bytesToWrite;
remain -= bytesToWrite;
}
if(currentPageOffset + positionInPage > size)
{
size = currentPageOffset + positionInPage;
}
}
/**
* {@inheritDoc}
*/
@Override
public final void clear() throws IOException
{
checkClosed();
// keep only the first page, discard all other pages
pageHandler.markPagesAsFree(pageIndexes, 1, pageCount - 1);
pageCount = 1;
// change to first page if we are not already there
if (currentPagePositionInPageIndexes > 0)
{
currentPage = pageHandler.readPage(pageIndexes[0]);
currentPagePositionInPageIndexes = 0;
currentPageOffset = 0;
}
positionInPage = 0;
size = 0;
currentPageContentChanged = false;
}
/**
* {@inheritDoc}
*/
@Override
public long getPosition() throws IOException
{
checkClosed();
return currentPageOffset + positionInPage;
}
/**
* {@inheritDoc}
*/
@Override
public void seek(long seekToPosition) throws IOException
{
checkClosed();
/*
* for now we won't allow to seek past end of buffer; this can be changed by adding new pages as needed
*/
if (seekToPosition > size)
{
throw new EOFException();
}
if (seekToPosition < 0)
{
throw new IOException("Negative seek offset: " + seekToPosition);
}
if ((seekToPosition >= currentPageOffset) && (seekToPosition <= currentPageOffset + pageSize))
{
// within same page
positionInPage = (int) (seekToPosition - currentPageOffset);
}
else
{
// have to go to another page
// check if current page needs to be written to file
if (currentPageContentChanged)
{
pageHandler.writePage(pageIndexes[currentPagePositionInPageIndexes], currentPage);
currentPageContentChanged = false;
}
int newPagePosition = (int) (seekToPosition / pageSize);
currentPage = pageHandler.readPage(pageIndexes[newPagePosition]);
currentPagePositionInPageIndexes = newPagePosition;
currentPageOffset = ((long)currentPagePositionInPageIndexes) * pageSize;
positionInPage = (int) (seekToPosition - currentPageOffset);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isClosed()
{
return pageHandler == null;
}
/**
* {@inheritDoc}
*/
@Override
public int peek() throws IOException
{
int result = read();
if (result != -1)
{
rewind(1);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public void rewind(int bytes) throws IOException
{
seek(currentPageOffset + positionInPage - bytes);
}
/**
* {@inheritDoc}
*/
@Override
public byte[] readFully(int len) throws IOException
{
byte[] b = new byte[len];
int n = 0;
do
{
int count = read(b, n, len - n);
if (count < 0)
{
throw new EOFException();
}
n += count;
} while (n < len);
return b;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEOF() throws IOException
{
checkClosed();
return currentPageOffset + positionInPage >= size;
}
/**
* {@inheritDoc}
*/
@Override
public int available() throws IOException
{
checkClosed();
return (int) Math.min(size - (currentPageOffset + positionInPage), Integer.MAX_VALUE);
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws IOException
{
checkClosed();
if (currentPageOffset + positionInPage >= size)
{
return -1;
}
if (! ensureAvailableBytesInPage(false))
{
// should not happen, we checked it before
throw new IOException("Unexpectedly no bytes available for read in buffer.");
}
return currentPage[positionInPage++] & 0xff;
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] b) throws IOException
{
return read(b, 0, b.length);
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] b, int off, int len) throws IOException
{
checkClosed();
if (currentPageOffset + positionInPage >= size)
{
return -1;
}
int remain = (int) Math.min(len, size - (currentPageOffset + positionInPage));
int totalBytesRead = 0;
int bOff = off;
while (remain > 0)
{
if (! ensureAvailableBytesInPage(false))
{
// should not happen, we checked it before
throw new IOException("Unexpectedly no bytes available for read in buffer.");
}
int readBytes = Math.min(remain, pageSize - positionInPage);
System.arraycopy(currentPage, positionInPage, b, bOff, readBytes);
positionInPage += readBytes;
totalBytesRead += readBytes;
bOff += readBytes;
remain -= readBytes;
}
return totalBytesRead;
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException
{
if (pageHandler != null) {
pageHandler.markPagesAsFree(pageIndexes, 0, pageCount);
pageHandler = null;
pageIndexes = null;
currentPage = null;
currentPageOffset = 0;
currentPagePositionInPageIndexes = -1;
positionInPage = 0;
size = 0;
}
}
/**
* While calling finalize is normally discouraged we will have to
* use it here as long as closing a scratch file buffer is not
* done in every case. Currently {@link COSStream} creates new
* buffers without closing the old one - which might still be
* used.
*
* Enabling debugging one will see if there are still cases
* where the buffer is not closed.
*/
@Override
protected void finalize() throws Throwable
{
try
{
if ((pageHandler != null) && LOG.isDebugEnabled())
{
LOG.debug("ScratchFileBuffer not closed!");
}
close();
}
finally
{
super.finalize();
}
}
}