/*
* 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 com.tom_roush.pdfbox.io;
import java.io.EOFException;
import java.io.IOException;
/**
* 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;
/**
* 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 < {@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);
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;
}
}
}