/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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 org.kaaproject.kaa.server.common.nosql.cassandra.dao;
import com.google.common.collect.Sets;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.UDTValue;
import com.datastax.driver.core.UserType;
import com.datastax.driver.mapping.annotations.ClusteringColumn;
import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.Defaults;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import com.datastax.driver.mapping.annotations.Transient;
import com.datastax.driver.mapping.annotations.UDT;
import org.kaaproject.kaa.server.common.nosql.cassandra.dao.client.CassandraClient;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CassandraEntityMapper<T> {
private static final Map<Class<?>, CassandraEntityMapper<?>> mappers = new HashMap<>();
private final List<String> nonKeyColumns = new ArrayList<>();
private final List<String> keyColumns = new ArrayList<>();
private final Map<String, PropertyDescriptor> fieldDescMap = new HashMap<>();
private Map<String, Map<Class<?>, CassandraEntityMapper<?>>> udtMappers = new HashMap<>();
private String name;
/**
* Create new instance of <code>CassandraEntityMapper</code>.
*
* @param entityClass is entity class
* @param cassandraClient os cassandra client
*/
public CassandraEntityMapper(Class<T> entityClass, CassandraClient cassandraClient) {
Table tableAnnotation = entityClass.getAnnotation(Table.class);
UDT udtAnnotation = entityClass.getAnnotation(UDT.class);
this.name = tableAnnotation != null ? tableAnnotation.name() : udtAnnotation.name();
for (Field field : entityClass.getDeclaredFields()) {
if (field.isSynthetic()
|| (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
continue;
}
if (field.getAnnotation(Transient.class) != null) {
continue;
}
Column column = field.getAnnotation(Column.class);
com.datastax.driver.mapping.annotations.Field fieldAnnotation
= field.getAnnotation(com.datastax.driver.mapping.annotations.Field.class);
if (column != null || fieldAnnotation != null) {
if (column != null) {
Class<? extends TypeCodec<?>> codecClass = column.codec();
if (!codecClass.equals(Defaults.NoCodec.class)) {
try {
@SuppressWarnings("unchecked")
TypeCodec<Object> instance = (TypeCodec<Object>) codecClass
.newInstance();
cassandraClient.getSession()
.getCluster()
.getConfiguration()
.getCodecRegistry()
.register(instance);
} catch (Exception exception) {
throw new IllegalArgumentException(String.format(
"Cannot create an instance of custom codec %s for field %s",
codecClass, field
), exception);
}
}
}
String name = column != null ? column.name() : fieldAnnotation.name();
if (field.isAnnotationPresent(PartitionKey.class)
|| field.isAnnotationPresent(ClusteringColumn.class)) {
keyColumns.add(name);
} else {
nonKeyColumns.add(name);
}
String fieldName = field.getName();
try {
fieldDescMap.put(name, new PropertyDescriptor(
fieldName, field.getDeclaringClass()));
} catch (IntrospectionException exception) {
throw new IllegalArgumentException(
"Cannot find matching getter and setter for field '" + fieldName + "'");
}
Set<Class<?>> udts = findUdts(field.getGenericType());
if (!udts.isEmpty()) {
Map<Class<?>, CassandraEntityMapper<?>> udtMap = new HashMap<>();
for (Class<?> udtClass : udts) {
if (!udtMap.containsKey(udtClass)) {
udtMap.put(
udtClass, getEntityMapperForClass(udtClass, cassandraClient));
}
}
udtMappers.put(name, udtMap);
}
}
}
}
/**
* Factory method: found instance of <code>CassandraEntityMapper</code> from <code>mappers</code>
* by <code>clazz</code> or create instance if not found and return it.
*
* @param clazz using for searching instance of <code>CassandraEntityMapper</code> in
* <code>mappers</code>
* @param cassandraClient using when creating new instance of CassandraEntityMapper
*/
@SuppressWarnings("unchecked")
public static <E> CassandraEntityMapper<E> getEntityMapperForClass(
Class<E> clazz,
CassandraClient cassandraClient
) {
CassandraEntityMapper<?> mapper = mappers.get(clazz);
if (mapper == null) {
if (clazz.isAnnotationPresent(Table.class)) {
cassandraClient.getMapper(clazz);
}
mapper = new CassandraEntityMapper<E>(clazz, cassandraClient);
mappers.put(clazz, mapper);
}
return (CassandraEntityMapper<E>) mapper;
}
static boolean mapsToCollection(Class<?> klass) {
return mapsToList(klass) || mapsToSet(klass) || mapsToMap(klass);
}
private static boolean mapsToList(Class<?> klass) {
return List.class.isAssignableFrom(klass);
}
private static boolean mapsToSet(Class<?> klass) {
return Set.class.isAssignableFrom(klass);
}
private static boolean mapsToMap(Class<?> klass) {
return Map.class.isAssignableFrom(klass);
}
static boolean isMappedUdt(Class<?> klass) {
return klass.isAnnotationPresent(UDT.class);
}
static Set<Class<?>> findUdts(Type type) {
Set<Class<?>> udts = findUdts(type, null);
return (udts == null)
? Collections.<Class<?>>emptySet()
: udts;
}
private static Set<Class<?>> findUdts(Type type, Set<Class<?>> udts) {
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
Type raw = pt.getRawType();
if ((raw instanceof Class)) {
Class<?> klass = (Class<?>) raw;
if (mapsToCollection(klass)) {
Type[] childTypes = pt.getActualTypeArguments();
udts = findUdts(childTypes[0], udts);
if (mapsToMap(klass)) {
udts = findUdts(childTypes[1], udts);
}
}
}
} else if (type instanceof Class) {
Class<?> klass = (Class<?>) type;
if (isMappedUdt(klass)) {
if (udts == null) {
udts = Sets.newHashSet();
}
udts.add(klass);
}
}
return udts;
}
public String getName() {
return name;
}
public List<String> getNonKeyColumnNames() {
return nonKeyColumns;
}
public List<String> getKeyColumnNames() {
return keyColumns;
}
/**
* Get column value for name, search value in field <code>fieldDescMap</code> or create new.
*
* @param name is name for which search column value
* @param entity using for create column value
* @param cassandraClient using for create column value
* @return column value
*/
public Object getColumnValueForName(String name,
Object entity,
CassandraClient cassandraClient) {
PropertyDescriptor pd = fieldDescMap.get(name);
try {
Object value = pd.getReadMethod().invoke(entity);
Map<Class<?>, CassandraEntityMapper<?>> udtMap = udtMappers.get(name);
if (udtMap != null && value != null) {
return convertValue(value, udtMap, cassandraClient);
}
return value;
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("Could not get field '" + pd.getName() + "'");
} catch (Exception execption) {
throw new IllegalStateException(
"Unable to access getter for '"
+ pd.getName() + "' in " + entity.getClass().getName(), execption);
}
}
private Object convertValue(Object value,
Map<Class<?>, CassandraEntityMapper<?>> udtMap,
CassandraClient cassandraClient) {
Class<?> valueClass = value.getClass();
if (mapsToCollection(valueClass)) {
if (mapsToList(valueClass)) {
List<?> valList = (List<?>) value;
List<Object> list = new ArrayList<>(valList.size());
for (Object elem : valList) {
list.add(convertValue(elem, udtMap, cassandraClient));
}
value = list;
} else if (mapsToSet(valueClass)) {
Set<?> valSet = (Set<?>) value;
Set<Object> set = new HashSet<>(valSet.size());
for (Object elem : valSet) {
set.add(convertValue(elem, udtMap, cassandraClient));
}
value = set;
} else if (mapsToMap(valueClass)) {
Map<?, ?> valMap = (Map<?, ?>) value;
Map<Object, Object> map = new HashMap<>(valMap.size());
for (Object elem : map.keySet()) {
Object elemVal = map.get(elem);
map.put(
convertValue(elem, udtMap, cassandraClient),
convertValue(elemVal, udtMap, cassandraClient));
}
value = map;
}
} else if (udtMap.containsKey(valueClass)) {
return convertValue(value, udtMap.get(valueClass), cassandraClient);
}
return value;
}
@SuppressWarnings("unchecked")
private UDTValue convertValue(Object value,
CassandraEntityMapper<?> mapper,
CassandraClient cassandraClient) {
String keyspace = cassandraClient.getSession().getLoggedKeyspace();
UserType userType = cassandraClient.getSession()
.getCluster()
.getMetadata()
.getKeyspace(keyspace)
.getUserType(mapper.getName());
UDTValue udtValue = userType.newValue();
for (String name : mapper.getNonKeyColumnNames()) {
Object fieldValue = mapper.getColumnValueForName(name, value, cassandraClient);
if (fieldValue != null) {
udtValue.set(name, fieldValue, (Class<Object>) fieldValue.getClass());
} else {
udtValue.setToNull(name);
}
}
return udtValue;
}
}