/*
* Copyright 2008 Niclas Hedhman.
*
* Licensed 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.qi4j.entitystore.swift;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import org.qi4j.api.entity.EntityReference;
/**
* For Slot 0
* [version] - 4 bytes
* [noOfEntries] - 4 bytes
* [slotSize] - 4 bytes
*
* For Slot 1..n
* [isExtended] - 1 byte
* [position] - 8 bytes
* [identity] - [slotSize-16] bytes
*/
public class IdentityFile
{
private static final int CURRENT_VERSION = 1;
private RandomAccessFile identityStore;
private int entries;
private int slotSize;
private boolean closed;
private BucketManager bucketManager;
private IdentityFile( RandomAccessFile store, File bucketDir, int slotSize, int entries )
throws IOException
{
this.closed = false;
identityStore = store;
bucketManager = new BucketManager( bucketDir );
this.slotSize = slotSize;
this.entries = entries;
}
int entries()
{
return entries;
}
long find( EntityReference reference )
throws IOException
{
if( closed )
{
throw new IdentityFileClosedException();
}
if( reference.identity().length() > slotSize - 16 )
{
throw new IdentityTooLongException( reference );
}
final int slot = getSlot( reference );
identityStore.seek( slot * slotSize );
boolean isExtended = identityStore.readBoolean();
if( !isExtended )
{
long pos = identityStore.readLong();
String idString = identityStore.readUTF();
if( idString.length() == 0 )
{
return -1;
}
EntityReference foundReference = new EntityReference( idString );
if( foundReference.equals( reference ) )
{
return pos;
}
return -1;
}
RandomAccessFile buckets = bucketManager.get( slot );
int next = 0;
while( next * slotSize < buckets.length() )
{
buckets.seek( next * slotSize );
boolean isUsed = buckets.readBoolean();
long pos = buckets.readLong();
EntityReference foundReference = new EntityReference( buckets.readUTF() );
if( isUsed && foundReference.equals( reference ) )
{
return pos;
}
next++;
}
return -1;
}
void remember( EntityReference reference, long pos )
throws IOException
{
if( closed )
{
throw new IdentityFileClosedException();
}
if( reference.identity().length() > slotSize - 16 )
{
throw new IdentityTooLongException( reference );
}
final int slot = getSlot( reference );
identityStore.seek( slot * slotSize );
boolean isExtended = identityStore.readBoolean();
if( isExtended )
{
RandomAccessFile bucket = bucketManager.get( slot );
bucket.seek( 0 );
int next = 0;
while( next * slotSize < bucket.length() )
{
bucket.seek( next * slotSize );
boolean isUsed = bucket.readBoolean();
if( !isUsed )
{
break;
}
next++;
}
bucket.seek( next * slotSize );
bucket.writeBoolean( true );
bucket.writeLong( pos );
bucket.writeUTF( reference.identity() );
fillExtras( bucket, next, slotSize );
}
else
{
long existingPos = identityStore.readLong();
if( existingPos == -1 )
{
// Not used yet.
identityStore.seek( slot * slotSize );
identityStore.writeBoolean( false );
identityStore.writeLong( pos );
identityStore.writeUTF( reference.identity() );
}
else
{
// Move existing record over to a new bucket.
RandomAccessFile bucket = bucketManager.get( slot );
bucket.seek( 0 );
bucket.writeBoolean( true );
bucket.writeLong( existingPos );
bucket.writeUTF( identityStore.readUTF() );
fillExtras( bucket, 0, slotSize );
bucket.seek( slotSize );
bucket.writeBoolean( true );
bucket.writeLong( pos );
bucket.writeUTF( reference.identity() );
fillExtras( bucket, 1, slotSize );
identityStore.seek( slot * slotSize );
identityStore.writeBoolean( true );
identityStore.writeLong( -1 );
identityStore.writeUTF( "" );
fillExtras( identityStore, slot, slotSize );
}
}
}
void drop( EntityReference reference )
throws IOException
{
if( closed )
{
throw new IdentityFileClosedException();
}
if( reference.identity().length() > slotSize - 16 )
{
throw new IdentityTooLongException( reference );
}
final int slot = getSlot( reference );
identityStore.seek( slot * slotSize );
boolean isExtended = identityStore.readBoolean();
if( isExtended )
{
RandomAccessFile buckets = bucketManager.get( slot );
int next = 0;
while( next * slotSize < buckets.length() )
{
buckets.seek( next * slotSize );
boolean isUsed = buckets.readBoolean();
buckets.readLong(); //ignore, should probably be changed to skip(8);
EntityReference foundReference = new EntityReference( buckets.readUTF() );
if( isUsed && foundReference.equals( reference ) )
{
buckets.seek( next * slotSize );
buckets.writeBoolean( false );
return;
}
next++;
}
}
else
{
identityStore.readLong(); // ignore the pos
EntityReference foundReference = new EntityReference( identityStore.readUTF() );
if( reference.equals( foundReference ) )
{
// found, no erase.
identityStore.seek( slot * slotSize );
identityStore.writeBoolean( false );
identityStore.writeLong( -1 );
identityStore.writeUTF( "" );
fillExtras( identityStore, slot, slotSize );
}
}
}
private int getSlot( EntityReference identity )
{
int hashCode = identity.hashCode();
hashCode = hashCode < 0 ? -hashCode : hashCode;
return 1 + hashCode % entries;
}
public void close()
throws IOException
{
bucketManager.close();
identityStore.close();
closed = true;
}
private static void initialize( RandomAccessFile newFile, int entries, int slotSize )
throws IOException
{
for( int i = 1; i <= entries + 1; i++ )
{
newFile.seek( i * slotSize );
newFile.writeBoolean( false ); // Extended
newFile.writeLong( -1 ); // Position
newFile.writeUTF( "" ); // Identity
fillExtras( newFile, i, slotSize );
}
newFile.seek( 0 );
newFile.writeInt( CURRENT_VERSION );
newFile.writeInt( entries );
newFile.writeInt( slotSize );
}
private static void fillExtras( RandomAccessFile accessFile, int slot, int slotSize )
throws IOException
{
long pointer = accessFile.getFilePointer();
long fillTo = ( slot + 1 ) * slotSize - 1;
long arraysize = fillTo - pointer;
if( arraysize < 0 )
{
System.err.println( "Negative Array Size detected:" + arraysize );
}
byte[] extras = new byte[(int) arraysize];
accessFile.write( extras );
}
public static IdentityFile use( File identityDir )
throws MalformedIdentityDirectoryException, IOException
{
File idFile = new File( identityDir, "id-hash.data" );
if( !idFile.exists() )
{
throw new MalformedIdentityDirectoryException( identityDir );
}
File bucketDir = new File( identityDir, "buckets" );
if( !bucketDir.exists() )
{
throw new MalformedIdentityDirectoryException( identityDir );
}
RandomAccessFile store = new RandomAccessFile( idFile, "rw" );
int version = store.readInt(); // Read Version
int entries = store.readInt(); // Read entries
int slotSize = store.readInt(); // Read slotSize
return new IdentityFile( store, bucketDir, slotSize, entries );
}
public static IdentityFile create( File identityDir, int slotSize, int idEntries )
throws IOException
{
FileUtils.delete( identityDir );
identityDir.mkdirs();
File idFile = new File( identityDir, "id-hash.data" );
RandomAccessFile store = new RandomAccessFile( idFile, "rw" );
initialize( store, idEntries, slotSize );
File bucketDir = new File( identityDir, "buckets" );
return new IdentityFile( store, bucketDir, slotSize, idEntries );
}
}