package me.arin.jacass;
import me.arin.jacass.annotations.Indexed;
import me.arin.jacass.annotations.IndexedProperty;
import me.arin.jacass.annotations.Model;
import me.arin.jacass.annotations.SimpleProperty;
import me.arin.jacass.serializer.PrimitiveSerializer;
import me.prettyprint.cassandra.dao.Command;
import me.prettyprint.cassandra.service.Keyspace;
import org.apache.cassandra.thrift.*;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.reflect.MethodUtils;
import org.safehaus.uuid.UUIDGenerator;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
/**
* Base model class - extend this yo
*/
abstract public class BaseModel {
protected String key;
protected static final int DEFAULT_MAX_COLUMNS = 100;
protected RowPath rowPath;
protected Map<String, ColumnInfo> columnInfos;
protected Serializer serializer;
protected HashMap<String, byte[]> originalIndexValues = new HashMap<String, byte[]>();
private Executor executor;
public BaseModel() {
}
protected int getMaxNumColumns() {
return DEFAULT_MAX_COLUMNS;
}
public String getKey() {
return key;
}
/**
* Get the row key for this object - and optionally generate one
* if we don't already have one
*
* @param generateIfNull Generate a key if its null
* @return The row key
*/
public String getKey(boolean generateIfNull) {
if (null == key && generateIfNull) {
key = generateKey();
}
return key;
}
/**
* You can use a custom class to (de)serialize the byte[] from the
* columns in the row associated with this object
* <p/>
* By default a PrimitiveSerializer is used which only deals with Java
* primitive types
*
* @return A Serializer
*/
public Serializer getSerializer() {
if (serializer == null) {
serializer = new PrimitiveSerializer();
}
return serializer;
}
/**
* Set the row key
*
* @param key
*/
public void setKey(String key) {
this.key = key;
}
/**
* Same as load() but exceptions are swallowed...
* You probably shouldn't ever use this
*
* @param key
* @param swallowException
* @return This object
*/
public BaseModel load(String key, boolean swallowException) {
try {
return load(key);
} catch (JacassException e) {
return null;
}
}
/**
* Load the data from a row in Cassandra into this object
*
* @param key The row key
* @return this
* @throws JacassException
*/
public BaseModel load(String key) throws JacassException {
setKey(key);
List<Column> columns = getColumns();
if (columns == null || columns.isEmpty()) {
return null;
}
injectColumns(columns);
return this;
}
/**
* Generate a row key
*
* @return A new row key
*/
public String generateKey() {
return this.getClass().getSimpleName().toLowerCase()
+ "."
+ UUIDGenerator.getInstance().generateRandomBasedUUID().toString();
}
/**
* Execute a Command
*
* @param command
* @param <T>
* @return The result of executing the Command aka: result of the Cassandra thrift call
* @throws Exception
*/
protected <T> T execute(Command<T> command) throws JacassException {
try {
Executor executor = getExectutor();
return executor.execute(this, command, getConsistencyLevel());
} catch (Exception e) {
throw new JacassException(e);
}
}
private <T> Executor getExectutor() {
if (executor == null) {
executor = Executor.get(getRowPath().getKeyspace());
}
return executor;
}
protected ConsistencyLevel getConsistencyLevel() {
return ConsistencyLevel.ONE;
}
/**
* See the other get()
*
* @param rowKeys
* @return Map indexed by the rowkey
*/
public Map<String, BaseModel> load(final String[] rowKeys) {
return load(Arrays.asList(rowKeys));
}
/**
* Multiget objects by their row keys
*
* @param rowKeys
* @return Map indexed by the rowkey
*/
public Map<String, BaseModel> load(final List<String> rowKeys) {
getRowPath();
final ColumnParent columnParent = new ColumnParent(rowPath.getColumnFamily());
String superColumn = rowPath.getSuperColumn();
if (!"".equals(superColumn)) {
columnParent.setSuper_column(superColumn.getBytes());
}
final SlicePredicate sp = new SlicePredicate();
sp.setSlice_range(new SliceRange(new byte[]{}, new byte[]{}, false, getMaxNumColumns()));
Command<Map<String, List<Column>>> command = new Command<Map<String, List<Column>>>() {
@Override
public Map<String, List<Column>> execute(Keyspace keyspace) throws Exception {
return keyspace.multigetSlice(rowKeys, columnParent, sp);
}
};
Map<String, BaseModel> rtn = new HashMap<String, BaseModel>();
try {
copyMap(rtn,execute(command));
} catch (Exception e) {
e.printStackTrace();
}
return rtn;
}
/**
* Get the RowPath for this object
* <p/>
* RowPath is the Keyspace -> CF [-> SC] that leads us to this row
*
* @return The row path
*/
protected RowPath getRowPath() {
if (rowPath == null) {
setupRowPath();
}
return rowPath;
}
/**
* Examine the @Model annotation to setup the rowPath field
*/
protected void setupRowPath() {
Annotation annotation = this.getClass().getAnnotation(Model.class);
Model a = (Model) annotation;
rowPath = new RowPath(a.keyspace(), a.columnFamily(), a.superColumn());
}
/**
* If an @Indexed annotation exists then you can override what CF the
* index data is stored in. If no @Indexed annotation exists then index data is
* stored in the CF specified in @Model
*
* @return The name of the CF to hold index data in
*/
protected String getIndexColumnFamily() {
Annotation annotation = this.getClass().getAnnotation(Indexed.class);
if (annotation == null) {
return getRowPath().getColumnFamily();
}
Indexed indexed = (Indexed) annotation;
return indexed.columnFamily();
}
/**
* Go thru the members of this class and figure out which ones need to be persisted
* and indexed
* <p/>
* All members with @SimpleProperty & @IndexedProperty will be returned along with some
* meta data about em (class, index info, etc)
*
* @return Info about all the members we need to persist
*/
protected Map<String, ColumnInfo> getColumnInfos() {
if (columnInfos == null) {
// have a map of the fieldname to columnInfo object
// need a map of columnName to field
columnInfos = new HashMap<String, ColumnInfo>();
Field[] declaredFields = this.getClass().getDeclaredFields();
for (Field field : declaredFields) {
SimpleProperty mp = field.getAnnotation(SimpleProperty.class);
IndexedProperty idx = field.getAnnotation(IndexedProperty.class);
if (mp != null || idx != null) {
ColumnInfo ci = new ColumnInfo(field);
if (idx != null) {
ci.setIndexData(new IndexInfo(idx.required(), idx.unique()));
}
columnInfos.put(field.getName(), ci);
}
}
}
return columnInfos;
}
/**
* Persist this object
*
* @return this
*/
public BaseModel save() throws JacassException {
final Map<String, List<Column>> cfMap = getCFMap();
Command<Void> command = new Command<Void>() {
@Override
public Void execute(Keyspace keyspace) throws Exception {
keyspace.batchInsert(getKey(true), cfMap, null);
return null;
}
};
List<Column> columns = cfMap.get(getRowPath().getColumnFamily());
for (Column column : columns) {
final String columnName = new String(column.getName());
final IndexInfo indexInfo = columnInfos.get(columnName).getIndexData();
if (indexInfo == null) {
continue;
}
Class columnType;
Object columnValue;
try {
ColumnInfo columnInfo = columnInfos.get(columnName);
columnType = columnInfo.getCls();
columnValue = columnInfo.getField().get(this);
} catch (Exception e) {
throw new JacassIndexException("Could not manage index data for " + columnName, e);
}
if (indexInfo.isRequired() && columnValue == null) {
throw new JacassIndexException(columnName + " is required and cannot be null");
}
final byte[] ogValue = originalIndexValues.get(columnName);
final ColumnCrud columnCrud = getExectutor().getColumnCrud();
if (ogValue != null) {
byte[] newValue = column.getValue();
if (!Arrays.equals(ogValue, newValue)) {
columnCrud.remove(getIndexColumnKey(column));
if (indexInfo.isUnique()) {
} else {
}
System.out.println("heh");
// TODO: delete old index
// TODO: create new index
}
}
}
execute(command);
return this;
}
/**
* // unique
* CF {
* column_name.unique_index : {
* value: object_key
* }
* }
*
* // not unique
* CF {
* column_name.value : {
* object_key : value
* }
* }
*
*
* @param column
* @return
*/
private ColumnKey getIndexColumnKey(Column column) {
return new ColumnKey(getRowPath().getKeyspace(), getRowPath().getSuperColumn(),
getRowPath().getColumnFamily(), "idxKey", "idxColumn");
}
/**
* Remove this object from Cassandra
*
* @return true
*/
public boolean remove() throws JacassException {
Command<Void> command = new Command<Void>() {
@Override
public Void execute(Keyspace keyspace) throws Exception {
keyspace.remove(getKey(), getColumnPath(getRowPath()));
return null;
}
};
execute(command);
return true;
}
/**
* Generate a ColumnPath from a RowPath
*
* @param rp
* @return
*/
private ColumnPath getColumnPath(RowPath rp) {
ColumnPath cp = new ColumnPath(rp.getColumnFamily());
String superColumn = rp.getSuperColumn();
if (!"".equals(superColumn)) {
cp.setSuper_column(superColumn.getBytes());
}
return cp;
}
/**
* Convenience method for remove()
*
* @param modelClass The class of the BaseModel
* @param key row key
* @return The success of this operation true|false
*/
public static boolean remove(Class modelClass, String key) throws JacassException {
BaseModel m;
try {
m = (BaseModel) modelClass.newInstance();
} catch (Exception e) {
throw new JacassException("Could not instanciate new " + modelClass.getSimpleName(), e);
}
m.setKey(key);
return m.remove();
}
/**
* Get a map of Columns to save into Cassandra
*
* @return A map of Column objects
* @throws JacassException
*/
public Map<String, List<Column>> getCFMap() throws JacassException {
getColumnInfos();
List<Column> columnList = new ArrayList<Column>();
for (String columnName : columnInfos.keySet()) {
String getterName = (new StringBuilder("get").append(StringUtils.capitalize(columnName))).toString();
Method method = MethodUtils.getAccessibleMethod(this.getClass(), getterName, new Class[]{});
if (method == null) {
throw new JacassException("No getter for " + columnName);
}
try {
Object value = method.invoke(this);
if (value != null)
columnList.add(new Column(columnName.getBytes(),
getSerializer().toBytes(columnInfos.get(columnName).getCls(),
value),
System.currentTimeMillis()));
} catch (Exception e) {
throw new JacassException("Could not serialize columns", e);
}
}
HashMap<String, List<Column>> rtn = new HashMap<String, List<Column>>();
rtn.put(getRowPath().getColumnFamily(), columnList);
return rtn;
}
/**
* Fetch Column info for this object from Cassandra
*
* @return Slice of Column objects from Cassandra
* @throws JacassException
*/
protected List<Column> getColumns() throws JacassException {
final RowPath rp = getRowPath();
final ColumnParent columnParent = new ColumnParent(rp.getColumnFamily());
String superColumn = rp.getSuperColumn();
if (!"".equals(superColumn)) {
columnParent.setSuper_column(superColumn.getBytes());
}
final SlicePredicate sp = new SlicePredicate();
sp.setSlice_range(new SliceRange(new byte[]{}, new byte[]{}, false, getMaxNumColumns()));
Command<List<Column>> command = new Command<List<Column>>() {
@Override
public List<Column> execute(Keyspace keyspace) throws Exception {
return keyspace.getSlice(getKey(), columnParent, sp);
}
};
try {
return execute(command);
} catch (Exception e) {
throw new JacassException(e);
}
}
/**
* Inject the values from Cassandra Columns into member vars of this object
*
* @param columns Columns from Cassandra
* @throws JacassException
*/
protected void injectColumns(List<Column> columns) throws JacassException {
getColumnInfos();
for (Column column : columns) {
String columnName = new String(column.getName());
String setterName = (new StringBuilder("set").append(StringUtils.capitalize(columnName))).toString();
ColumnInfo ci = columnInfos.get(columnName);
Class columnType = ci.getCls();
if (null == columnType) {
continue;
}
Object castData = null;
try {
castData = getSerializer().fromBytes(columnType, column.getValue());
if (ci.isIndexed()) {
originalIndexValues.put(columnName, column.getValue());
}
} catch (IOException e) {
throw new JacassException("Could not get value for " + columnName, e);
}
if (castData == null) {
continue;
}
Method method = MethodUtils.getAccessibleMethod(this.getClass(), setterName, columnType);
if (method != null) {
try {
method.invoke(this, castData);
} catch (Exception e) {
throw new JacassException("Could not call " + setterName, e);
}
} else {
throw new JacassException("No setter for " + column);
}
}
}
public String getKeyspace() {
return getRowPath().getKeyspace();
}
/**
* Load range of available objects
*
* @param startKey
* @param finishKey
* @return Map indexed by the rowkey
*/
public Map<String, BaseModel> load(final String startKey, final String finishKey) {
getRowPath();
final ColumnParent columnParent = new ColumnParent(rowPath.getColumnFamily());
String superColumn = rowPath.getSuperColumn();
if (!"".equals(superColumn)) {
columnParent.setSuper_column(superColumn.getBytes());
}
final SlicePredicate sp = new SlicePredicate();
sp.setSlice_range(new SliceRange(new byte[]{}, new byte[]{}, false, getMaxNumColumns()));
Command<Map<String, List<Column>>> command = new Command<Map<String, List<Column>>>() {
@Override
public Map<String, List<Column>> execute(Keyspace keyspace) throws Exception {
return keyspace.getRangeSlice(columnParent, sp, startKey, finishKey, getMaxNumColumns());
}
};
Map<String, BaseModel> rtn = new HashMap<String, BaseModel>();
try {
copyMap(rtn, execute(command));
} catch (Exception e) {
e.printStackTrace();
}
return rtn;
}
private void copyMap(Map<String, BaseModel> dest, Map<String, List<Column>> source) throws InstantiationException, IllegalAccessException, JacassException
{
for (String k : source.keySet()) {
List<Column> columns = source.get(k);
if (columns == null || columns.isEmpty()) {
continue;
}
BaseModel bm = this.getClass().newInstance();
bm.setKey(k);
bm.injectColumns(columns);
dest.put(k, bm);
}
}
}