/*
* Copyright 2010 Impetus Infotech.
*
* 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 com.impetus.kundera.client;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.SuperColumn;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wyki.cassandra.pelops.KeyDeletor;
import org.wyki.cassandra.pelops.Mutator;
import org.wyki.cassandra.pelops.Pelops;
import org.wyki.cassandra.pelops.Policy;
import org.wyki.cassandra.pelops.Selector;
import com.impetus.kundera.CassandraClient;
import com.impetus.kundera.Constants;
import com.impetus.kundera.ejb.EntityManagerImpl;
import com.impetus.kundera.loader.DBType;
import com.impetus.kundera.metadata.EntityMetadata;
import com.impetus.kundera.property.PropertyAccessException;
import com.impetus.kundera.property.PropertyAccessorFactory;
import com.impetus.kundera.property.PropertyAccessorHelper;
import com.impetus.kundera.proxy.EnhancedEntity;
/**
* Client implementation using Pelops. http://code.google.com/p/pelops/
*
* @author animesh.kumar
* @since 0.1
*/
public class PelopsClient implements CassandraClient {
/** The Constant poolName. */
private static final String poolName = "Main";
/** array of cassandra hosts. */
private String[] contactNodes;
/** default port. */
private int defaultPort;
/** The closed. */
private boolean closed = false;
/** log for this class. */
private static Log log = LogFactory.getLog(PelopsClient.class);
/**
* default constructor.
*/
public PelopsClient() {
}
/* @see com.impetus.kundera.CassandraClient#connect() */
@Override
public final void connect() {
Pelops.addPool(poolName, contactNodes, defaultPort, false, null,
new Policy());
}
/* @see com.impetus.kundera.CassandraClient#shutdown() */
@Override
public final void shutdown() {
Pelops.shutdown();
closed = true;
}
/**
* Checks if is open.
*
* @return true, if is open
*/
public final boolean isOpen() {
return !closed;
}
/*
* @see com.impetus.kundera.CassandraClient#writeColumns(java.lang.String,
* java.lang.String, java.lang.String, org.apache.cassandra.thrift.Column[])
*/
@Override
public final void writeColumns(String keyspace, String columnFamily,
String rowId, List<EntityMetadata.Column>columns,EnhancedEntity e) throws Exception {
if (!isOpen()) {
throw new PersistenceException("PelopsClient is closed.");
}
PelopsClient.ThriftRow tf = toThriftRow(e,columns,columnFamily);
Mutator mutator = Pelops.createMutator(poolName, keyspace);
mutator.writeColumns(tf.getId(), columnFamily, Arrays.asList(tf.getColumns().toArray(new Column[0])));
mutator.execute(ConsistencyLevel.ONE);
}
@Override
public void writeColumns(EntityManagerImpl em, EnhancedEntity e, EntityMetadata m) throws Exception {
throw new PersistenceException("Not yet implemented");
}
/*
* @see
* com.impetus.kundera.CassandraClient#writeSuperColumns(java.lang.String,
* java.lang.String, java.lang.String,
* org.apache.cassandra.thrift.SuperColumn[])
*/
@Override
public final void writeSuperColumns(String keyspace, String columnFamily,
String rowId, SuperColumn... superColumns) throws Exception {
if (!isOpen()) {
throw new PersistenceException("PelopsClient is closed.");
}
Mutator mutator = Pelops.createMutator(poolName, keyspace);
for (SuperColumn sc : superColumns) {
mutator.writeSubColumns(rowId, columnFamily, sc.getName(), sc
.getColumns());
}
mutator.execute(ConsistencyLevel.ONE);
}
/*
* @see com.impetus.kundera.CassandraClient#loadColumns(java.lang.String,
* java.lang.String, java.lang.String)
*/
@Override
public final <E> E loadColumns(EntityManagerImpl em,Class<E> clazz,
String keyspace, String columnFamily,
String rowId, EntityMetadata m) throws Exception{
if (!isOpen()) {
throw new PersistenceException("PelopsClient is closed.");
}
Selector selector = Pelops.createSelector(poolName, keyspace);
List<Column> columns = selector.getColumnsFromRow(rowId, columnFamily, Selector
.newColumnsPredicateAll(true, 10), ConsistencyLevel.ONE);
E e;
if (null == columns || columns.size() == 0) {
e = null;
} else {
e = fromThriftRow(em,clazz, m, this.new ThriftRow(rowId, columnFamily, columns));
}
return e;
}
/*
* @see com.impetus.kundera.CassandraClient#loadColumns(java.lang.String,
* java.lang.String, java.lang.String[])
*/
@Override
public final /*Map<String, List<Column>>*/ <E> List<E> loadColumns(EntityManagerImpl em, Class<E> clazz, String keyspace,
String columnFamily, EntityMetadata m,String... rowIds) throws Exception {
if (!isOpen()) {
throw new PersistenceException("PelopsClient is closed.");
}
Selector selector = Pelops.createSelector(poolName, keyspace);
Map<String, List<Column>> map = selector.getColumnsFromRows(Arrays.asList(rowIds), columnFamily,
Selector.newColumnsPredicateAll(false, 1000),
ConsistencyLevel.ONE);
List<E> entities = new ArrayList<E>();
// Iterate and populate entities
for (Map.Entry<String, List<Column>> entry : map.entrySet()) {
String id = entry.getKey();
List<Column> columns = entry.getValue();
if (entry.getValue().size() == 0) {
log.debug("@Entity not found for id: " + id);
continue;
}
E e = fromThriftRow(em,clazz, m, this.new ThriftRow(id, columnFamily,
columns));
entities.add(e);
}
return entities;
}
/*
* @see com.impetus.kundera.CassandraClient#delete(java.lang.String,
* java.lang.String, java.lang.String)
*/
@Override
public final void delete(String keyspace, String columnFamily, String rowId)
throws Exception {
if (!isOpen()) {
throw new PersistenceException("PelopsClient is closed.");
}
KeyDeletor keyDeletor = Pelops.createKeyDeletor(poolName, keyspace);
keyDeletor
.deleteColumnFamily(rowId, columnFamily, ConsistencyLevel.ONE);
}
/*
* @see
* com.impetus.kundera.CassandraClient#loadSuperColumns(java.lang.String,
* java.lang.String, java.lang.String, java.lang.String[])
*/
@Override
public final List<SuperColumn> loadSuperColumns(String keyspace,
String columnFamily, String rowId, String... superColumnNames)
throws Exception {
if (!isOpen())
throw new PersistenceException("PelopsClient is closed.");
Selector selector = Pelops.createSelector(poolName, keyspace);
return selector.getSuperColumnsFromRow(rowId, columnFamily, Selector
.newColumnsPredicate(superColumnNames), ConsistencyLevel.ONE);
}
/*
* @see
* com.impetus.kundera.CassandraClient#loadSuperColumns(java.lang.String,
* java.lang.String, java.lang.String[])
*/
@Override
public final Map<String, List<SuperColumn>> loadSuperColumns(
String keyspace, String columnFamily, String... rowIds)
throws Exception {
if (!isOpen())
throw new PersistenceException("PelopsClient is closed.");
Selector selector = Pelops.createSelector(poolName, keyspace);
return selector.getSuperColumnsFromRows(Arrays.asList(rowIds),
columnFamily, Selector.newColumnsPredicateAll(false, 1000),
ConsistencyLevel.ONE);
}
/* @see com.impetus.kundera.CassandraClient#getCassandraClient() */
@Override
public final Cassandra.Client getCassandraClient() throws Exception {
return Pelops.getDbConnPool(poolName).getConnection().getAPI();
}
/**
* Sets the contact nodes.
*
* @param contactNodes
* the contactNodes to set
*/
@Override
public final void setContactNodes(String... contactNodes) {
this.contactNodes = contactNodes;
}
/**
* Sets the default port.
*
* @param defaultPort
* the defaultPort to set
*/
@Override
public final void setDefaultPort(int defaultPort) {
this.defaultPort = defaultPort;
}
/* @see java.lang.Object#toString() */
@Override
public final String toString() {
StringBuilder builder = new StringBuilder();
builder.append("PelopsClient [contactNodes=");
builder.append(Arrays.toString(contactNodes));
builder.append(", defaultPort=");
builder.append(defaultPort);
builder.append(", closed=");
builder.append(closed);
builder.append("]");
return builder.toString();
}
/**
* Utility class that represents a row in Cassandra DB.
*
* @author animesh.kumar
*/
public class ThriftRow {
/** Id of the row. */
private String id;
/** name of the family. */
private String columnFamilyName;
/** list of thrift columns from the row. */
private List<Column> columns;
/**
* default constructor.
*/
public ThriftRow() {
columns = new ArrayList<Column>();
}
/**
* The Constructor.
*
* @param id
* the id
* @param columnFamilyName
* the column family name
* @param columns
* the columns
*/
public ThriftRow(String id, String columnFamilyName, List<Column> columns) {
this.id = id;
this.columnFamilyName = columnFamilyName;
this.columns = columns;
}
/**
* Gets the id.
*
* @return the id
*/
public String getId() {
return id;
}
/**
* Sets the id.
*
* @param id
* the key to set
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the column family name.
*
* @return the columnFamilyName
*/
public String getColumnFamilyName() {
return columnFamilyName;
}
/**
* Sets the column family name.
*
* @param columnFamilyName
* the columnFamilyName to set
*/
public void setColumnFamilyName(String columnFamilyName) {
this.columnFamilyName = columnFamilyName;
}
/**
* Gets the columns.
*
* @return the columns
*/
public List<Column> getColumns() {
return columns;
}
/**
* Sets the columns.
*
* @param columns
* the columns to set
*/
public void setColumns(List<Column> columns) {
this.columns = columns;
}
/**
* Adds the column.
*
* @param column
* the column
*/
public void addColumn(Column column) {
columns.add(column);
}
}
/**
* From thrift row.
*
* @param <E>
* the element type
* @param clazz
* the clazz
* @param m
* the m
* @param cr
* the cr
* @return the e
* @throws Exception
* the exception
*/
private <E> E fromThriftRow(EntityManagerImpl em, Class<E> clazz, EntityMetadata m,
PelopsClient.ThriftRow cr) throws Exception {
// Instantiate a new instance
E e = clazz.newInstance();
// Set row-key. Note: @Id is always String.
PropertyAccessorHelper.set(e, m.getIdProperty(), cr.getId());
// Iterate through each column
for (Column c : cr.getColumns()) {
String name = PropertyAccessorFactory.STRING.fromBytes(c.getName());
byte[] value = c.getValue();
if (null == value) {
continue;
}
// check if this is a property?
EntityMetadata.Column column = m.getColumn(name);
if (null == column) {
// it could be some relational column
EntityMetadata.Relation relation = m.getRelation(name);
if (relation == null) {
continue;
}
String foreignKeys = PropertyAccessorFactory.STRING
.fromBytes(value);
Set<String> keys = deserializeKeys(foreignKeys);
em.getEntityResolver().populateForeignEntities(
e, cr.getId(), relation, keys.toArray(new String[0]));
}
else {
try {
PropertyAccessorHelper.set(e, column.getField(), value);
} catch (PropertyAccessException pae) {
log.warn(pae.getMessage());
}
}
}
return e;
}
/**
* Helper method to convert @Entity to ThriftRow
*
* @param e
* the e
* @param m
* the m
* @return the base data accessor. thrift row
* @throws Exception
* the exception
*/
private PelopsClient.ThriftRow toThriftRow(EnhancedEntity e,
List<EntityMetadata.Column> columnsLst, String colmunFamily) throws Exception {
// timestamp to use in thrift column objects
long timestamp = System.currentTimeMillis();
PelopsClient.ThriftRow cr = this.new ThriftRow();
// column-family name
cr.setColumnFamilyName(colmunFamily);
// id
cr.setId(e.getId());
List<Column> columns = new ArrayList<Column>();
// Iterate through each column-meta and populate that with field values
for (EntityMetadata.Column column : columnsLst) {
Field field = column.getField();
String name = column.getName();
try {
byte[] value = PropertyAccessorHelper.get(e.getEntity(), field);
columns.add(new Column(PropertyAccessorFactory.STRING
.toBytes(name), value, timestamp));
} catch (PropertyAccessException exp) {
log.warn(exp.getMessage());
}
}
// add foreign keys
for (Map.Entry<String, Set<String>> entry : e.getForeignKeysMap()
.entrySet()) {
String property = entry.getKey();
Set<String> foreignKeys = entry.getValue();
String keys = serializeKeys(foreignKeys);
if (null != keys) {
columns.add(new Column(PropertyAccessorFactory.STRING
.toBytes(property), PropertyAccessorFactory.STRING
.toBytes(keys), timestamp));
}
}
cr.setColumns(columns);
return cr;
}
/**
* Splits foreign keys into Set.
*
* @param foreignKeys
* the foreign keys
* @return the set
*/
private Set<String> deserializeKeys(String foreignKeys) {
Set<String> keys = new HashSet<String>();
if (null == foreignKeys || foreignKeys.isEmpty()) {
return keys;
}
String array[] = foreignKeys.split(Constants.SEPARATOR);
for (String element : array) {
keys.add(element);
}
return keys;
}/**
* Creates a string representation of a set of foreign keys by combining
* them together separated by "~" character.
*
* Note: Assumption is that @Id will never contain "~" character. Checks for
* this are not added yet.
*
* @param foreignKeys
* the foreign keys
* @return the string
*/
protected String serializeKeys(Set<String> foreignKeys) {
if (null == foreignKeys || foreignKeys.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (String key : foreignKeys) {
if (sb.length() > 0) {
sb.append(Constants.SEPARATOR);
}
sb.append(key);
}
return sb.toString();
}
@Override
public void setKeySpace(String keySpace) {
}
@Override
public DBType getType() {
return DBType.CASSANDRA;
}
}