blob: 96eb6b0b5981c8e349f5cc19f203469a39d8f613 [file] [log] [blame]
/*
* 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;
/**
* <code>true</code> 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 (newSize<pageIndexes.length)
{
if (pageIndexes.length == Integer.MAX_VALUE)
{
throw new IOException("Maximum buffer size reached.");
}
newSize = Integer.MAX_VALUE;
}
int[] newPageIndexes = new int[newSize];
System.arraycopy(pageIndexes, 0, newPageIndexes, 0, pageCount);
pageIndexes = newPageIndexes;
}
int newPageIdx = pageHandler.getNewPage();
pageIndexes[pageCount] = newPageIdx;
currentPagePositionInPageIndexes = pageCount;
currentPageOffset = ((long)pageCount) * pageSize;
pageCount++;
currentPage = new byte[pageSize];
positionInPage = 0;
}
/**
* {@inheritDoc}
*/
@Override
public long length() throws IOException
{
return size;
}
/**
* Ensures the current page has at least one byte left
* ({@link #positionInPage} in &lt; {@link #pageSize}).
*
* <p>If 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.</p>
*
* @param addNewPageIfNeeded if <code>true</code> it is allowed to add a new page in case
* we are currently at end of last buffer page
*
* @return <code>true</code> if we were successful positioning pointer before end of page;
* we might return <code>false</code> 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);
if (seekToPosition % pageSize == 0 && seekToPosition == size)
{
newPagePosition--; // PDFBOX-4756: Prevent seeking a non-yet-existent page...
}
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 boolean isEOF() throws IOException
{
checkClosed();
return currentPageOffset + positionInPage >= size;
}
/**
* {@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, 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.
*
* <p>Enabling debugging one will see if there are still cases
* where the buffer is not closed.</p>
*/
@Override
protected void finalize() throws Throwable
{
try
{
if ((pageHandler != null) && LOG.isDebugEnabled())
{
LOG.debug("ScratchFileBuffer not closed!");
}
close();
}
finally
{
super.finalize();
}
}
@Override
public RandomAccessReadView createView(long startPosition, long streamLength) throws IOException
{
return new RandomAccessReadView(this, startPosition, streamLength);
}
}