blob: 862b276b8590891f39cd09f7a5865f66c00bf7f9 [file] [log] [blame]
package org.apache.commons.jcs3.auxiliary.disk.indexed;
/*
* 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.
*/
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
import org.apache.commons.jcs3.log.Log;
import org.apache.commons.jcs3.log.LogManager;
/** Provides thread safe access to the underlying random access file. */
public class IndexedDisk implements AutoCloseable
{
/** The size of the header that indicates the amount of data stored in an occupied block. */
public static final byte HEADER_SIZE_BYTES = 4;
/** The serializer. */
private final IElementSerializer elementSerializer;
/** The logger */
private static final Log log = LogManager.getLog(IndexedDisk.class);
/** The path to the log directory. */
private final String filepath;
/** The data file. */
private final FileChannel fc;
/**
* Constructor for the Disk object
* <p>
* @param file
* @param elementSerializer
* @throws IOException
*/
public IndexedDisk(final File file, final IElementSerializer elementSerializer)
throws IOException
{
this.filepath = file.getAbsolutePath();
this.elementSerializer = elementSerializer;
this.fc = FileChannel.open(file.toPath(),
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE);
}
/**
* This reads an object from the given starting position on the file.
* <p>
* The first four bytes of the record should tell us how long it is. The data is read into a byte
* array and then an object is constructed from the byte array.
* <p>
* @return Serializable
* @param ded
* @throws IOException
* @throws ClassNotFoundException
*/
protected <T> T readObject(final IndexedDiskElementDescriptor ded)
throws IOException, ClassNotFoundException
{
String message = null;
boolean corrupted = false;
final long fileLength = fc.size();
if (ded.pos > fileLength)
{
corrupted = true;
message = "Record " + ded + " starts past EOF.";
}
else
{
final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
fc.read(datalength, ded.pos);
datalength.flip();
final int datalen = datalength.getInt();
if (ded.len != datalen)
{
corrupted = true;
message = "Record " + ded + " does not match data length on disk (" + datalen + ")";
}
else if (ded.pos + ded.len > fileLength)
{
corrupted = true;
message = "Record " + ded + " exceeds file length.";
}
}
if (corrupted)
{
log.warn("\n The file is corrupt: \n {0}", message);
throw new IOException("The File Is Corrupt, need to reset");
}
final ByteBuffer data = ByteBuffer.allocate(ded.len);
fc.read(data, ded.pos + HEADER_SIZE_BYTES);
data.flip();
return elementSerializer.deSerialize(data.array(), null);
}
/**
* Moves the data stored from one position to another. The descriptor's position is updated.
* <p>
* @param ded
* @param newPosition
* @throws IOException
*/
protected void move(final IndexedDiskElementDescriptor ded, final long newPosition)
throws IOException
{
final ByteBuffer datalength = ByteBuffer.allocate(HEADER_SIZE_BYTES);
fc.read(datalength, ded.pos);
datalength.flip();
final int length = datalength.getInt();
if (length != ded.len)
{
throw new IOException("Mismatched memory and disk length (" + length + ") for " + ded);
}
// TODO: more checks?
long readPos = ded.pos;
long writePos = newPosition;
// header len + data len
int remaining = HEADER_SIZE_BYTES + length;
final ByteBuffer buffer = ByteBuffer.allocate(16384);
while (remaining > 0)
{
// chunk it
final int chunkSize = Math.min(remaining, buffer.capacity());
buffer.limit(chunkSize);
fc.read(buffer, readPos);
buffer.flip();
fc.write(buffer, writePos);
buffer.clear();
writePos += chunkSize;
readPos += chunkSize;
remaining -= chunkSize;
}
ded.pos = newPosition;
}
/**
* Writes the given byte array to the Disk at the specified position.
* <p>
* @param data
* @param ded
* @return true if we wrote successfully
* @throws IOException
*/
protected boolean write(final IndexedDiskElementDescriptor ded, final byte[] data)
throws IOException
{
final long pos = ded.pos;
if (log.isTraceEnabled())
{
log.trace("write> pos={0}", pos);
log.trace("{0} -- data.length = {1}", fc, data.length);
}
if (data.length != ded.len)
{
throw new IOException("Mismatched descriptor and data lengths");
}
final ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE_BYTES);
headerBuffer.putInt(data.length);
// write the header
headerBuffer.flip();
int written = fc.write(headerBuffer, pos);
assert written == HEADER_SIZE_BYTES;
//write the data
final ByteBuffer dataBuffer = ByteBuffer.wrap(data);
written = fc.write(dataBuffer, pos + HEADER_SIZE_BYTES);
return written == data.length;
}
/**
* Serializes the object and write it out to the given position.
* <p>
* TODO: make this take a ded as well.
* @param obj
* @param pos
* @throws IOException
*/
protected <T> void writeObject(final T obj, final long pos)
throws IOException
{
final byte[] data = elementSerializer.serialize(obj);
write(new IndexedDiskElementDescriptor(pos, data.length), data);
}
/**
* Returns the raf length.
*
* @return the length of the file.
* @throws IOException If an I/O error occurs.
*/
protected long length()
throws IOException
{
return fc.size();
}
/**
* Closes the raf.
* <p>
* @throws IOException
*/
@Override
public void close()
throws IOException
{
fc.close();
}
/**
* Sets the raf to empty.
* <p>
* @throws IOException
*/
protected synchronized void reset()
throws IOException
{
log.debug("Resetting Indexed File [{0}]", filepath);
fc.truncate(0);
fc.force(true);
}
/**
* Truncates the file to a given length.
* <p>
* @param length the new length of the file
* @throws IOException
*/
protected void truncate(final long length)
throws IOException
{
log.info("Truncating file [{0}] to {1}", filepath, length);
fc.truncate(length);
}
/**
* This is used for debugging.
* <p>
* @return the file path.
*/
protected String getFilePath()
{
return filepath;
}
/**
* Tests if the length is 0.
* @return true if the if the length is 0.
* @throws IOException If an I/O error occurs.
* @since 3.1
*/
protected boolean isEmpty() throws IOException
{
return length() == 0;
}
}