/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.index.tree;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import org.apache.sis.util.ArgumentChecks;
/**
* Define a default abstract {@link TreeElementMapper} class, to store tree Identifiers on disk.
*
* @author Remi Marechal (Geomatys).
*/
public abstract class ChannelTreeElementMapper<E> implements TreeElementMapper<E> {
/**
* FileTreeElementMapper identifier.
*/
private final static int TREE_ELT_MAP_NUMBER = 44034146;
/**
* Default byte buffer length.
*/
private final static int DEFAULT_BUFFER_LENGTH = 4096;
/**
* Channel to read and write data informations and tree identifiers.
*/
private final SeekableByteChannel inOutChannel;
/**
* Size in {@code Byte} of object which will be mapped.
*/
private final int objSize;
/**
* {@link FileChannel} position just after write or read file head.<br/>
* Its also file position of first Node red or written.
*/
private final int beginPosition;
/**
* Last mapped object {@link FileChannel} position.
*/
private int maxPosition;
/**
* {@link ByteBuffer} to read and write data informations and tree identifiers from file on hard disk.
*/
protected final ByteBuffer byteBuffer;
/**
* ByteBuffer Length.
*/
private final int bufferLength;
/**
* Store element on disk at file path emplacement.<br/>
* Consist to store identifier use during {@link Tree} insertion.<br/>
* A default buffer length of {@link #DEFAULT_BUFFER_LENGTH} is choose.
*
* @param channel channel where to read/write datas
* @param objectSize size in {@code Byte} unit of elements which will be store.
* @see AbstractTree#insert(java.lang.Object)
* @throws IOException if problem during {@link RandomAccessFile} creation.
*/
protected ChannelTreeElementMapper(final SeekableByteChannel channel, final int objectSize) throws IOException {
this(channel, DEFAULT_BUFFER_LENGTH, objectSize);
}
/**
* Store element on disk at file path emplacement.<br/>
* Consist to store identifier use during {@link Tree} insertion.
*
* @param channel channel where to read/write datas
* @param bufferLength length in Byte unit of the buffer which read and write on hard drive.
* @param objectSize size in {@code Byte} unit of elements which will be store.
* @see AbstractTree#insert(java.lang.Object)
* @throws IOException if problem during {@link RandomAccessFile} creation.
*/
protected ChannelTreeElementMapper(final SeekableByteChannel channel, final int bufferLength, final int objectSize) throws IOException {
ArgumentChecks.ensureNonNull("SeekableByteChannel", channel);
ArgumentChecks.ensureStrictlyPositive("buffer length", bufferLength);
ArgumentChecks.ensureStrictlyPositive("object size", objectSize);
inOutChannel = channel;
// Ensure buffer capacity is a multiple of object length.
final int div = bufferLength / objectSize;
this.bufferLength = div * objectSize;
byteBuffer = ByteBuffer.allocateDirect(this.bufferLength);
channel.position(0);
//-- head length in byte
final int headLength = 13;
final ByteOrder bO;
boolean isRead = channel.size() >= headLength;
byteBuffer.position(0);
byteBuffer.limit(13);
if (isRead) {
inOutChannel.read(byteBuffer);
byteBuffer.flip();
assert byteBuffer.position() == 0;
assert byteBuffer.limit() == headLength;
// read mapper identifier
final int identifier = byteBuffer.getInt();
if (identifier != TREE_ELT_MAP_NUMBER)
throw new IllegalArgumentException("input file don't contain TreeElementMapper information.");
// read ByteOrder
final boolean fbool = byteBuffer.get() == 1;
bO = (fbool) ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
byteBuffer.order(bO);
// read object size
objSize = byteBuffer.getInt();
// read maxposition
maxPosition = byteBuffer.getInt();
assert byteBuffer.position() == headLength;
if (objSize != objectSize)
throw new IOException("Input data does not match given parameter \"object length\" : "+objSize+" vs "+objectSize);
} else {
objSize = objectSize;
bO = ByteOrder.nativeOrder();
/****************************** write head *****************************/
//-- default jdk byteOrder to write treeIdentifier and ByteOrder
byteBuffer.putInt(TREE_ELT_MAP_NUMBER);
byteBuffer.put((byte)((bO == ByteOrder.LITTLE_ENDIAN) ? 1 : 0));
byteBuffer.order(bO);
byteBuffer.putInt(objSize);
byteBuffer.putInt(0);
byteBuffer.flip();
assert byteBuffer.position() == 0;
assert byteBuffer.limit() == headLength;
inOutChannel.write(byteBuffer);
/******************************* end head *****************************/
}
//-- init buffer
byteBuffer.position(0);
byteBuffer.flip();
beginPosition = (int) inOutChannel.position();
assert beginPosition == headLength;
if (isRead) pullBuffer();
else maxPosition = beginPosition;
}
/**
* Write object in output stream already positioned.
*
* @param Object which will be write.
*/
protected abstract void writeObject(E Object) throws IOException ;
/**
* Read object from inpout stream already positioned.
*
* @return red object.
*/
protected abstract E readObject() throws IOException ;
/**
* Compare and return true if the two object are equals else false.
*
* @param objectA
* @param objectB
* @return return true if the two object are equals else false.
*/
protected abstract boolean areEquals(E objectA, E objectB);
/**
* {@inheritDoc }.
*/
@Override
public synchronized int getTreeIdentifier(E object) throws IOException {
int treeIdentifier = 1;
for (int currentPos = beginPosition; currentPos < maxPosition; currentPos += objSize) {
adjustBuffer(treeIdentifier);
final E currentObject = readObject();
if (areEquals(currentObject, object)) return treeIdentifier;
treeIdentifier++;
}
if (maxPosition == beginPosition) throw new IllegalStateException(this.getClass().getName()+".getTreeIdentifier() : you must set object before.");
throw new IllegalStateException(this.getClass().getName()+".getTreeIdentifier() : impossible to find treeIdentifier for object : "+object.toString());
}
/**
* {@inheritDoc }.
*/
@Override
public synchronized void setTreeIdentifier(E object, int treeIdentifier) throws IOException {
adjustBuffer(treeIdentifier);
writeObject(object);
maxPosition += objSize;
}
/**
* {@inheritDoc }.
*/
@Override
public synchronized E getObjectFromTreeIdentifier(int treeIdentifier) throws IOException {
adjustBuffer(treeIdentifier);
return readObject();
}
/**
* Put all attributes like just after constructor.
*/
@Override
public synchronized void clear() throws IOException {
pushBuffer();
inOutChannel.position(beginPosition);
pullBuffer();
maxPosition = beginPosition;
}
/**
* Close stream and FileChannel.
*
* @throws IOException
*/
@Override
public synchronized void close() throws IOException {
if (inOutChannel.isOpen()) {
pushBuffer();
writeMaxPosition();
inOutChannel.close();
}
}
/**
* Flush {@link #byteBuffer} into {@link #inOutChannel}.
*
* @throws IOException
*/
@Override
public synchronized void flush() throws IOException {
final long channelPos = inOutChannel.position();
pushBuffer();
writeMaxPosition();
inOutChannel.position(channelPos);
pullBuffer();
}
/**
* {@inheritDoc }
*/
@Override
public synchronized boolean isClosed() {
return !inOutChannel.isOpen();
}
/**
* Adjust buffer position relative to filechanel which contain data,
* and prepare bytebuffer position and limit for reading or writing action.
*
* @param treeIdentifier
* @throws IOException
*/
private void adjustBuffer(final int treeIdentifier) throws IOException {
long rwIndex = beginPosition + (treeIdentifier - 1) * objSize;
long channelPos = inOutChannel.position();
if (rwIndex < channelPos || (rwIndex + objSize) > channelPos + bufferLength) {
pushBuffer();
final long div = ((rwIndex - beginPosition) / bufferLength);
channelPos = div * bufferLength + beginPosition;
inOutChannel.position(channelPos);
pullBuffer();
}
rwIndex -= channelPos;
//-- only write that is necessarily
byteBuffer.limit(Math.max(byteBuffer.limit(), (int) rwIndex + objSize));
byteBuffer.position((int) rwIndex);
}
/**
* Write the {@link #byteBuffer} into {@link #inOutChannel}, but without any {@link SeekableByteChannel#position() } changements.<br>
* After buffer writing the channel position is rewind at its position when this method is called.<br>
* Moreover, its {@link ByteBuffer#position() } and its {@link ByteBuffer#limit() } are setted to zero.
*/
private void pushBuffer() throws IOException {
final long channelPosition = inOutChannel.position();
byteBuffer.flip();
int expectedWB = byteBuffer.remaining();
int writtenByte = 0;
while (writtenByte < expectedWB) {
writtenByte += inOutChannel.write(byteBuffer);
}
inOutChannel.position(channelPosition);
byteBuffer.position(0);
byteBuffer.flip();
}
/**
* Fill the {@link #byteBuffer} from {@link #inOutChannel} datas, but without any {@link SeekableByteChannel#position() } changements.<br>
* After buffer reading the channel position is rewind at its position when this method is called.<br>
* Moreover, its {@link ByteBuffer#position() } and its {@link ByteBuffer#limit() } are setted to zero.
*/
private void pullBuffer() throws IOException {
final long channelPosition = inOutChannel.position();
byteBuffer.clear();
inOutChannel.read(byteBuffer);
inOutChannel.position(channelPosition);
byteBuffer.position(0);
byteBuffer.flip();
}
/**
* Write {@link #maxPosition} into this file header.
*/
private void writeMaxPosition() throws IOException {
//step mapper Identifier, byteOrder and object size.
inOutChannel.position(9);
//-- write maxPos
byteBuffer.clear();
byteBuffer.putInt(maxPosition);
byteBuffer.flip();
inOutChannel.write(byteBuffer);
}
}