/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* FileChangeOutputStream.java
* Creation date: Aug 11, 2005.
* By: Edward Lam
*/
package org.openquark.util;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* Reads a file until a difference is noticed, then writes starting from the difference.
* If there is no difference between the file and the stream, the file is not modified.
*
* Typically, reading from a file is much faster than writing to it, so this class can yield big
* savings in situations where there is little or no change in a file's data.
*/
public final class FileChangeOutputStream extends OutputStream {
/** The size of the byte buffer into which file data will be read. */
private static final int BUF_SIZE = 4096; // Most classes are <4K.
/** The file channel backing this stream. */
private final FileChannel fileChannel;
/** Whether a difference has been observed between the file and the data to write. */
private boolean sawDifference;
/**
* The thread-local byte buffer, into which file data will be read.
* Used by lookupClassData() and loadFileData().
*/
private static ThreadLocal<ByteBuffer> threadLocalByteBuffer = new ThreadLocal<ByteBuffer>() {
@Override
protected synchronized ByteBuffer initialValue() {
// allocateDirect() doesn't help, since we have to copy the bytes out to an array in order to perform comparisons anyways.
return ByteBuffer.allocate(BUF_SIZE);
}
};
/**
* Constructor for a FileChangeOutputStream.
* @param fileName the name of the file on which to create a stream.
* @throws IOException
*/
public FileChangeOutputStream(String fileName) throws IOException {
this(new File(fileName));
}
/**
* Constructor for a FileChangeOutputStream.
* @param file the file on which to create a stream.
* @throws IOException
*/
public FileChangeOutputStream(File file) throws IOException {
// "rws" and "rwd" are also available.
fileChannel = new RandomAccessFile(file, "rw").getChannel(); // Creates the file if the file doesn't exist.
}
/**
* @return whether a difference has been observed between the data and the file.
*/
public boolean isDifferent() {
return sawDifference;
}
/**
* {@inheritDoc}
*/
// synchronized
@Override
public void write(byte[] data, int offset, int length) throws IOException {
if (sawDifference) {
// Really write the data.
reallyWrite(data, offset, length);
} else {
ByteBuffer byteBuffer = threadLocalByteBuffer.get();
int nBytesRead = 0;
do {
// read some bytes.
int readBytes = fileChannel.read(byteBuffer);
try {
if (readBytes < 0) {
// We are a point in the file where there are no bytes to read.
reallyWrite(data, offset, length);
sawDifference = true;
return;
}
if (!equalsArray(data, offset + nBytesRead, byteBuffer.array(), readBytes)) {
// We got to a point in the file where the bytes are different.
// Seek to the original position and write.
long currentPosition = fileChannel.position();
fileChannel.position(currentPosition - (nBytesRead + readBytes));
reallyWrite(data, offset, length);
sawDifference = true;
return;
}
} finally {
// Make sure the byte buffer is ready for reading again.
byteBuffer.clear();
}
// Haven't found a difference yet.
nBytesRead += readBytes;
} while (nBytesRead < length);
}
}
/**
* Write data to the file channel.
* @param data the data to write.
* @param offset the offset into the data array from which to write.
* @param length the length of the data to write.
* @throws IOException
*/
private void reallyWrite(byte[] data, int offset, int length) throws IOException {
// Really write the data.
ByteBuffer bufToWrite = ByteBuffer.wrap(data, offset, length);
do {
fileChannel.write(bufToWrite);
} while (bufToWrite.hasRemaining());
}
/**
* Check whether array elements are equal.
*
* @param a1 the first array to be tested.
* @param offset the offset into a1 from which to start testing.
* @param a2 the second array to be tested.
* @param len the number of elements to test for equality.
* @return true if the elements in the two arrays are equal.
*/
private static boolean equalsArray(byte[] a1, int offset, byte[] a2, int len) {
for (int i = 0, aIndex = offset; i < len; i++) {
if (a1[aIndex] != a2[i]) {
return false;
}
aIndex++;
}
return true;
}
/**
* {@inheritDoc}
*/
// synchronized
@Override
public void write(byte[] data) throws IOException {
write(data, 0, data.length);
}
/**
* {@inheritDoc}
*/
// synchronized
@Override
public void write(int data) throws IOException {
write(new byte[]{(byte)data});
}
/**
* {@inheritDoc}
*/
// synchronized
@Override
public void close() throws IOException {
long pos = fileChannel.position();
if (fileChannel.size() != pos) {
fileChannel.truncate(pos);
sawDifference = true;
}
fileChannel.close();
}
}