Thursday, August 12, 2010

cassandraentitytransfomer

I have been playing around with Cassandra lately using its Java (Thrift) client API. In order to map different classes (entities) to a list of ColumnOrSuperColumns, I have created a generic entity transformer.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.Mutation;

public abstract class CassandraEntityTransformer<T> {
  public static final String CHARSET = "UTF-8";

  public abstract List<ColumnOrSuperColumn> getColumns(T entity) throws Exception;
  public abstract T getEntity(List<ColumnOrSuperColumn> columns) throws Exception;

  public List<Mutation> listMutations(T oldEntity, T newEntity) throws Exception {
     List<Mutation> mutations = new ArrayList<Mutation>();
     List<ColumnOrSuperColumn> oldColumns = getColumns(oldEntity);
     List<ColumnOrSuperColumn> newColumns = getColumns(newEntity);

     if(oldColumns.size()!=newColumns.size())
        throw new IllegalArgumentException("incompatible entities");

     for(int i=0; i<newColumns.size(); i++) {
        if(!Arrays.equals(newColumns.get(i).getColumn().getValue(),
                oldColumns.get(i).getColumn().getValue())) {
           mutations.add(
               new Mutation().setColumn_or_supercolumn(newColumns.get(i)));
        }
     }
     return mutations;
  }

  protected ColumnOrSuperColumn createColumn(Column column) {
     ColumnOrSuperColumn c = new ColumnOrSuperColumn();
     c.setColumn(column);
     return c;
  }
}
Subclasses of this class are responsible for providing the columns of concrete entities, like for the token entity in the following example. I found this quite handy, especially as getMutations works out of the box for all entities.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;

public class CassandraTokenTransformer extends CassandraEntityTransformer<Token> {
   private static final String VALUE_COLUMN_NAME = "value";
   private static final String EMAIL_COLUMN_NAME = "email";

   @Override
   public List<ColumnOrSuperColumn> getColumns(Token token) throws Exception {
      List<ColumnOrSuperColumn> columns = new ArrayList<ColumnOrSuperColumn>();

      long timestamp = System.currentTimeMillis();

      // the value column.
      columns.add(createColumn(
              new Column(VALUE_COLUMN_NAME.getBytes(CHARSET),
              token.getValue().getBytes(CHARSET), timestamp)));

      // the email column.
      columns.add(createColumn(
              new Column(EMAIL_COLUMN_NAME.getBytes(CHARSET),
              token.getEmail().getBytes(CHARSET), timestamp)));

      return columns;
   }

   @Override
   public Token getEntity(List<ColumnOrSuperColumn> columns) {
      Token token = null;

      Map<String, String> fields = new HashMap<String, String>();
      for (ColumnOrSuperColumn column : columns) {
         fields.put(new String(column.column.name), 
                 new String(column.column.value));
      }
      if(fields.containsKey(VALUE_COLUMN_NAME))
         token = new Token(fields.get(VALUE_COLUMN_NAME),
                           fields.get(EMAIL_COLUMN_NAME));

      return token;
   }
}
Alternatives would have been to use an annotation based mapping mechanism for entities to Cassandra columns, or a JSON based API. Maybe they are out there, and I have just missed them?