Appendix A. Custom Serializer implementation

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));