package nl.tno.sensorstorm.particlemapper;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentMap;
import nl.tno.sensorstorm.api.annotation.Mapper;
import nl.tno.sensorstorm.api.annotation.TupleField;
import nl.tno.sensorstorm.api.particles.CustomParticlePojoMapper;
import nl.tno.sensorstorm.api.particles.Particle;
import nl.tno.sensorstorm.storm.SensorStormBolt;
import nl.tno.sensorstorm.storm.SensorStormSpout;
import org.jboss.netty.util.internal.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
/**
* Utility Class that can map {@link Tuple}s to {@link Particle}s and
* {@link Particle}s to {@link Values}. The class also has useful related
* methods.
*/
public class ParticleMapper {
public static final Logger log = LoggerFactory
.getLogger(ParticleMapper.class);
public static final int TIMESTAMP_IDX = 0;
public static final int PARTICLE_CLASS_IDX = 1;
public static final int PARTICLE_MINIMAL_FIELDS = 2;
public static final String TIMESTAMP_FIELD_NAME = "timestamp";
public static final String PARTICLE_CLASS_FIELD_NAME = "particleClass";
private static final String PARTICLE_TO_VALUES_METHOD_NAME = "particleToValues";
private static ConcurrentMap<Class<?>, CustomParticlePojoMapper<?>> customSerializers = new ConcurrentHashMap<>();
private static ConcurrentMap<Class<?>, Method> customSerializersMapMethods = new ConcurrentHashMap<>();
private static ConcurrentMap<Class<?>, ParticleClassInfo> particleClassInfos = new ConcurrentHashMap<>();
/**
* Private constructor, this class should not be instantiated.
*/
private ParticleMapper() {
}
/**
* Map a {@link Particle} to a {@link Values} object.
*
* @param particle
* {@link Particle} to map
* @return Corresponding {@link Values} object
*/
public static Values particleToValues(Particle particle) {
Class<? extends Particle> clazz = particle.getClass();
if (hasCustomSerializer(clazz)) {
CustomParticlePojoMapper<?> customSerializer = getCustomMapper(clazz);
Method serializeMethod = customSerializersMapMethods.get(clazz);
try {
return (Values) serializeMethod.invoke(customSerializer,
particle);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
log.error("Could not map particle to values", e);
return null;
}
} else {
return getParticleClassInfo(clazz).particleToValues(particle);
}
}
/**
* Map a {@link Particle} to a {@link Values} object and make sure it has
* expectedNrOfFields fields. This is useful for when a Spout or Bolt has
* declared more output fields than this {@link Particle} has.
*
* @param particle
* {@link Particle} to map
* @param expectedNrOfFields
* Desired number of fields
* @return Corresponding {@link Values} object
*/
public static Values particleToValues(Particle particle,
int expectedNrOfFields) {
Values values = particleToValues(particle);
if (values.size() > expectedNrOfFields) {
throw new IllegalArgumentException("Expected number of Fields ("
+ expectedNrOfFields
+ ") is smaller than the found number of fields ("
+ values.size() + ")");
}
while (values.size() < expectedNrOfFields) {
values.add(null);
}
return values;
}
/**
* Map a {@link Tuple} to a {@link Particle} when the type of the
* {@link Particle} is already known.
*
* @param <T>
* The type of the {@link Particle}
* @param tuple
* {@link Tuple} to be mapped
* @param clazz
* {@link Class} of the expected type of {@link Particle}
* @return The mapped {@link Tuple}
*/
@SuppressWarnings("unchecked")
public static <T extends Particle> T tupleToParticle(Tuple tuple,
Class<T> clazz) {
if (hasCustomSerializer(clazz)) {
return (T) getCustomMapper(clazz).tupleToParticle(tuple);
} else {
return getParticleClassInfo(clazz).tupleToParticle(tuple, clazz);
}
}
/**
* Map a {@link Tuple} to a {@link Particle} when the type of the
* {@link Particle} is not yet known.
*
* @param tuple
* {@link Tuple} to be mapped
* @return The mapped {@link Tuple}
*/
public static Particle tupleToParticle(Tuple tuple) {
Class<?> clazz = null;
ParticleClassInfo particleClassInfo;
try {
clazz = Class.forName(tuple.getString(PARTICLE_CLASS_IDX));
particleClassInfo = getParticleClassInfo(clazz);
} catch (ClassNotFoundException e) {
particleClassInfo = null;
}
if (particleClassInfo != null) {
return (Particle) particleClassInfo.tupleToParticle(tuple, clazz);
} else {
// Maybe it has a custom mapper?
for (CustomParticlePojoMapper<?> m : customSerializers.values()) {
if (m.canMapTuple(tuple)) {
return m.tupleToParticle(tuple);
}
}
// Could not find a custom mapper. Now what?
log.error("Could not find mapper for the tuple. If the particle has a custom mapper that the ParticleMapper doesn't know, tell the ParticleMapper by calling the ParticleMapper.inspectClass method.");
return null;
}
}
/**
* Check if a type of {@link Particle} has a
* {@link CustomParticlePojoMapper}.
*
* @param clazz
* Type of the {@link Particle}
* @return if the {@link Particle} has a custom serializer
*/
private static boolean hasCustomSerializer(Class<? extends Particle> clazz) {
for (Annotation a : clazz.getAnnotations()) {
if (a instanceof Mapper) {
return true;
}
}
return false;
}
/**
* Get the {@link Fields} present in type of {@link Particle}.
*
* @param clazz
* {@link Class} of the {@link Particle}
* @return {@link Fields} in the {@link Particle}
*/
public static Fields getFields(Class<? extends Particle> clazz) {
if (hasCustomSerializer(clazz)) {
return getCustomMapper(clazz).getFields();
} else {
return getParticleClassInfo(clazz).getFields();
}
}
/**
* Inspect a type of {@link Particle}. A {@link Particle} cannot be mapped
* until it is known by the {@link ParticleMapper}. No worries, the
* {@link SensorStormSpout} and {@link SensorStormBolt} usually take care of
* this.
*
* @param clazz
* Type of the {@link Particle} to inspect
*/
public static void inspectClass(Class<? extends Particle> clazz) {
getFields(clazz);
}
/**
* Get the {@link ParticleClassInfo} of a auto-mapped {@link Particle} type.
* One is created if it is not present yet.
*
* @param clazz
* {@link Class} of the {@link Particle}.
* @return {@link ParticleClassInfo} for this type of {@link Particle}
*/
private static ParticleClassInfo getParticleClassInfo(Class<?> clazz) {
ParticleClassInfo pci = particleClassInfos.get(clazz);
if (pci != null) {
return pci;
} else {
// Construct the ParticleClassInfo object.
// key = name of field, value = name in tuple
SortedMap<String, String> outputFields = new TreeMap<String, String>();
for (Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
for (Annotation a : f.getAnnotations()) {
if (a instanceof TupleField) {
String name = ((TupleField) a).name();
if ((name == null) || (name.length() == 0)) {
name = f.getName();
}
outputFields.put(f.getName(), name);
}
}
}
pci = new ParticleClassInfo(clazz, outputFields);
particleClassInfos.putIfAbsent(clazz, pci);
return particleClassInfos.get(clazz);
}
}
/**
* Get the {@link CustomParticlePojoMapper} of a manual-mapped
* {@link Particle} type. One is created if it is not present yet.
*
* @param clazz
* {@link Class} of the {@link Particle}.
* @return {@link CustomParticlePojoMapper} for this type of
* {@link Particle}
*/
private static CustomParticlePojoMapper<?> getCustomMapper(Class<?> clazz) {
CustomParticlePojoMapper<?> ps = customSerializers.get(clazz);
if (ps != null) {
return ps;
} else {
try {
for (Annotation a : clazz.getAnnotations()) {
if (a instanceof Mapper) {
Class<?> serializerClass = ((Mapper) a).value();
customSerializers.putIfAbsent(clazz,
(CustomParticlePojoMapper<?>) serializerClass
.newInstance());
// Find the (first) map method
CustomParticlePojoMapper<?> customSerializer = getCustomMapper(clazz);
for (Method m : customSerializer.getClass()
.getMethods()) {
if (m.getName().equals(
PARTICLE_TO_VALUES_METHOD_NAME)) {
customSerializersMapMethods.putIfAbsent(clazz,
m);
break;
}
}
return customSerializers.get(clazz);
}
}
} catch (InstantiationException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// should not be possible to get here
return null;
}
/**
* Merge two Fields objects. Duplicate fields are removed in the process. If
* one of the arguments is null the method will return a copy of the other
* Fields. If both are null the result is an empty Fields object.
*
* @param first
* The first Fields object (may be null)
* @param second
* The second Fields object (may be null)
* @return Fields object
*/
public static Fields mergeFields(Fields first, Fields second) {
List<String> copy;
if (first == null) {
copy = new ArrayList<String>();
} else {
copy = first.toList();
}
if (second != null) {
for (String s : second.toList()) {
if (!copy.contains(s)) {
copy.add(s);
}
}
}
return new Fields(copy);
}
/**
* Get the index of a Field in the {@link Tuple} of a {@link Particle}.
*
* @param clazz
* Type of {@link Particle}.
* @param fieldId
* Name of the field
* @return position of the field. -1 if not found.
*/
public static int getFieldIdx(Class<? extends Particle> clazz,
String fieldId) {
Fields fields = ParticleMapper.getFields(clazz);
int fieldNr = -1;
for (String field : fields) {
fieldNr++;
if (field != null) {
if (fieldId.equals(field)) {
return fieldNr;
}
}
}
return -1;
}
}