This appendix contains an example of how a custom Serializer
can be implemented and then used in a Database. The custom serializer,
the MoneySerializer is used for serializing
Money objects.
The Money object is an immutable Java
bean containing an amount and a Currency.
package org.helidb.javabank;
public class Money
{
  private final long m_amount;
  private final Currency m_currency;
  
  public Money(long amount, Currency currency)
  {
    m_amount = amount;
    m_currency = currency;
  }
  
  public long getAmount()
  {
    return m_amount;
  }
  
  public Currency getCurrency()
  {
    return m_currency;
  }
}Currency is an Enum.
package org.helidb.javabank;
// Persisting enums can be frustrating. There is a discussion of the topic in
// this Java Specialist's newsletter:
// http://www.javaspecialists.co.za/archive/Issue113.html
//
// This enum implementation uses a fairly naïve approach.  
public enum Currency
{
  KRONA(0), EURO(1), DOLLAR(2), POUND(3);
  
  // An unique index for each currency.
  private final int m_index;
  
  private Currency(int index)
  {
    m_index = index;
  }
  
  // Get this currency's unique index
  public int getIndex()
  {
    return m_index;
  }
  
  // Get the currency corresponding to the index value
  public static Currency valueOf(int index)
  {
    switch(index)
    {
      case 0: return KRONA;
      case 1: return EURO;
      case 2: return DOLLAR;
      case 3: return POUND;
      default: throw new IllegalArgumentException("Unknown index: " + index);
    }
  }
}The MoneySerializer implementation uses
serializers for primitive types to serialize the Money
object's amount and Currency.
package org.helidb.javabank; // This object serializes Money objects. It represents the currency with an // unsigned byte. This limits the number of currencies to 256. public class MoneySerializer implements Serializer<Money> { // This object contains no mutable internal state, so this singleton instance // may be used instead of instantiating it. public static final MoneySerializer INSTANCE = new MoneySerializer(); // The size of a long + an unsigned byte public static final int DATA_SIZE = LongSerializer.DATA_SIZE + UnsignedByteSerializer.DATA_SIZE; public int getSerializedSize() { return DATA_SIZE; } public boolean isNullValuesPermitted() { return false; } private Money interpretInternal(byte[] barr, int offset) { long amount = LongSerializer.INSTANCE.interpret( barr, offset, LongSerializer.DATA_SIZE); offset += LongSerializer.DATA_SIZE; Currency c = Currency.valueOf( UnsignedByteSerializer.INSTANCE.interpret( barr, offset, UnsignedByteSerializer.DATA_SIZE)); return new Money(amount, c); } public Money interpret(byte[] barr) { return interpretInternal(barr, 0); } public Money interpret(byte[] barr, int offset, int length) { if (length != DATA_SIZE) { throw new SerializationException("Invalid length " + length); } return interpretInternal(barr, offset); } public int serialize(Money m, byte[] barr, int offset) { LongSerializer.INSTANCE.serialize( m.getAmount(), barr, offset); offset += LongSerializer.DATA_SIZE; UnsignedByteSerializer.INSTANCE.serialize( (short) m.getCurrency().getIndex(), barr, offset); return DATA_SIZE; } public byte[] serialize(Money m) { byte[] res = new byte[DATA_SIZE]; serialize(m, res, 0); return res; } private void validateSize(int size) { if (size != DATA_SIZE) { throw new SerializationException("Invalid data size " + size); } } public Money read(RandomAccess ra, int size) { validateSize(size); byte[] barr = new byte[DATA_SIZE]; int noRead = ra.read(barr); if (noRead != DATA_SIZE) { throw new NotEnoughDataException(DATA_SIZE, noRead); } return interpret(barr); } public Money read(InputStream is, int size) { validateSize(size); byte[] barr = new byte[DATA_SIZE]; try { int noRead = is.read(barr); if (noRead != DATA_SIZE) { throw new NotEnoughDataException(DATA_SIZE, noRead); } } catch (IOException e) { // Rethrow as an unchecked exception throw new WrappedIOException(e); } return interpret(barr); } }
This is how the MoneySerializer may be
used in a database.
// Create a SimpleDatabase that uses a ConstantRecordSizeBPlusTreeBackend on the // database File f. The database contains Long keys (account numbers?) and // Money values. // A LogAdapter that logs to stdout and stderr. LogAdapterHolder lah = new LogAdapterHolder( new StdOutLogAdapter()); // A SimpleDatabase that have Long keys and Money values. Database<Long, Money> db = new SimpleDatabase<Long, Money, KeyAndValue<Long, Money>>( new ConstantRecordSizeBPlusTreeBackend<Long, Money>( // The NodeRepository is used by the BPlusTree // Use a caching node repository. new LruCacheNodeRepository<Long, Money>( // Use a FileBackedNodeRepositoryBuilder to build the file-backed // node repository. // The Long keys are Comparable, so we don't have to use a Comparator. new FileBackedNodeRepositoryBuilder<Long, Money>(). // Use a node size of 4096 bytes for the B+ Tree. This is a common // file system allocation size. setNodeSizeStrategy(new FixedSizeNodeSizeStrategy(4096)). // Have to use a null-aware serializer for the B+ Tree keys. setKeySerializer(LongNullSerializer.INSTANCE). // Here is our MoneySerializer setValueSerializer(new MoneySerializer()). // This pointer size sets the maximum size of the B+ Tree to // 2^(2*8) = 65536 bytes. setInternalPointerSize(2). setLogAdapterHolder(lah). // Adapt the File to a ReadWritableFile. create(new ReadWritableFileAdapter(f), false), // Set the maximum LRU cache size to 16 nodes. 16), false, lah), lah); // Insert a value db.insert(9019506L, new Money(1000000, Currency.KRONA));