| /* |
| * 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 < {@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); |
| } |
| |
| } |