Skip to content

Instantly share code, notes, and snippets.

@bonifaido
Last active December 16, 2017 20:27
Show Gist options
  • Save bonifaido/5141859 to your computer and use it in GitHub Desktop.
Save bonifaido/5141859 to your computer and use it in GitHub Desktop.
This utility class converts QuickFIX/J Messages into a human readable format.
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import quickfix.CharField;
import quickfix.DecimalField;
import quickfix.Field;
import quickfix.IntField;
import quickfix.Message;
import quickfix.StringField;
import quickfix.UtcTimeStampField;
import quickfix.field.BeginString;
import quickfix.field.CheckSum;
import quickfix.field.ClOrdID;
import quickfix.field.HandlInst;
import quickfix.field.OrdType;
import quickfix.field.Price;
import quickfix.field.Side;
import quickfix.field.Symbol;
import quickfix.field.TransactTime;
import quickfix.field.converter.UtcTimestampConverter;
import quickfix.fix42.NewOrderSingle;
/**
* TODO Groups are not handled at the moment.
*
* @author Nandor Kracser
*/
public class FixMessageUtil {
/**
* Marks a method if it's return value is cached.
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
private @interface Cached {
}
private static class FieldHelper {
public final int tag;
public final String name;
public final Method readMethod;
public FieldHelper(int tag, String name, Method readMethod) {
this.tag = tag;
this.name = name;
this.readMethod = readMethod;
}
}
private static class FieldClass {
public final Class<? extends Field> klass;
public final Constructor<? extends Field> constructor;
public final Class<?> constructorParameterType;
public FieldClass(Class<? extends Field> klass, Constructor<? extends Field> constructor, Class<?> firstParameterType) {
this.klass = klass;
this.constructor = constructor;
this.constructorParameterType = firstParameterType;
}
}
private static class FieldEntry {
public final String name;
public final String value;
public FieldEntry(String name, String value) {
this.name = name;
this.value = value;
}
}
private static ConcurrentMap<Class<?>, String> CLASS_SIMPLENAME_CACHE = new ConcurrentHashMap<>();
private static ConcurrentMap<Class<?>, List<FieldHelper>> FIELD_HELPER_CACHE = new ConcurrentHashMap<>();
private static ConcurrentMap<Class<?>, Map<Character, String>> FIELD_CONSTANT_CACHE = new ConcurrentHashMap<>();
private static ConcurrentMap<Class<?>, Map<String, Character>> FIELD_CONSTANT_CACHE_INVERSE = new ConcurrentHashMap<>();
private static ConcurrentMap<String, Class<? extends Message>> MESSAGE_CLASS_CACHE = new ConcurrentHashMap<>();
private static ConcurrentMap<String, FieldClass> FIELD_CLASS_CACHE = new ConcurrentHashMap<>();
private static void cacheFieldConstants(Class<?> klass) {
try {
klass.getConstructor(char.class);
} catch (Exception ex) {
// this a class has no constants
return;
}
if (FIELD_CONSTANT_CACHE.containsKey(klass)) {
// already cached this class
return;
}
Map<Character, String> fieldConstants = new HashMap<>();
Map<String, Character> fieldConstantsInverse = new HashMap<>();
for (java.lang.reflect.Field field : klass.getFields()) {
if (Modifier.isStatic(field.getModifiers()) && Modifier.isPublic(field.getModifiers())) {
try {
Character constant = field.getChar(null);
String name = field.getName();
fieldConstants.put(constant, name);
fieldConstantsInverse.put(name, constant);
} catch (Exception ex) {
// maybe it's not a char, let it be
}
}
}
FIELD_CONSTANT_CACHE.putIfAbsent(klass, fieldConstants);
FIELD_CONSTANT_CACHE_INVERSE.putIfAbsent(klass, fieldConstantsInverse);
}
private static Object getValue(Field f) {
Object value = f.getObject();
if (f instanceof CharField) {
Map<Character, String> fieldConstant = FIELD_CONSTANT_CACHE.get(f.getClass());
if (fieldConstant != null) {
value = fieldConstant.get((Character) value);
}
} else if (f instanceof UtcTimeStampField) {
value = UtcTimestampConverter.convert((Date) value, true);
}
return value;
}
@Cached
private static List<FieldHelper> getFieldHelpers(Class<?> c) throws Exception {
List<FieldHelper> fieldHelpers = FIELD_HELPER_CACHE.get(c);
if (fieldHelpers == null) {
fieldHelpers = new ArrayList<>();
PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(c).getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
Method readMethod = propertyDescriptor.getReadMethod();
if (readMethod != null && readMethod.getDeclaringClass() == c && readMethod.getName().startsWith("get")) {
Class<?> returnType = readMethod.getReturnType();
cacheFieldConstants(returnType);
int tag = returnType.getDeclaredField("FIELD").getInt(null);
String name = readMethod.getName().substring(3);
fieldHelpers.add(new FieldHelper(tag, name, readMethod));
}
}
FIELD_HELPER_CACHE.putIfAbsent(c, fieldHelpers);
}
return fieldHelpers;
}
private static Map<String, Object> inspect(Message m) throws Exception {
Map<String, Object> fieldValues = new HashMap<>(); // TODO can be replaced with List<Entry<String, Object>>
List<FieldHelper> fieldHelpers = getFieldHelpers(m.getClass());
for (FieldHelper fieldHelper : fieldHelpers) {
try {
// avoid uneccesary FieldNotFound exception creation
if (m.isSetField(fieldHelper.tag)) {
Field field = (Field) fieldHelper.readMethod.invoke(m);
Object value = getValue(field);
fieldValues.put(fieldHelper.name, value);
}
} catch (Exception e) {
// not likely to happen
}
}
return fieldValues;
}
@Cached
private static String getSimpleName(Class<? extends Message> klass) {
String simpleName = CLASS_SIMPLENAME_CACHE.get(klass);
if (simpleName == null) {
simpleName = klass.getSimpleName();
CLASS_SIMPLENAME_CACHE.putIfAbsent(klass, simpleName);
}
return simpleName;
}
public static String toString(Message m) {
StringBuilder sb = new StringBuilder(getSimpleName(m.getClass()));
sb.append('{');
try {
sb.append("BeginString=").append(m.getHeader().getString(BeginString.FIELD)).append(", ");
sb.append("BodyLength=").append(m.bodyLength());
for (Map.Entry<String, Object> field : inspect(m).entrySet()) {
sb.append(", ").append(field.getKey()).append('=').append(field.getValue());
}
if (m.isSetField(CheckSum.FIELD)) {
sb.append(", CheckSum=").append(m.getTrailer().getInt(CheckSum.FIELD));
}
} catch (Exception ex) {
return m.toString();
}
sb.append('}');
return sb.toString();
}
private static List<FieldEntry> getMessageFieldEntries(String msg) {
String fieldsStr = msg.substring(msg.indexOf('{') + 1, msg.lastIndexOf('}'));
String[] fieldPairs = fieldsStr.split(",");
List<FieldEntry> entries = new ArrayList<>(fieldPairs.length);
for (String fieldPair : fieldPairs) {
String[] nameValue = fieldPair.trim().split("=");
entries.add(new FieldEntry(nameValue[0], nameValue[1]));
}
return entries;
}
private static Field newFieldInstance(FieldClass fieldClass, String value) throws Exception {
Class<?> constructorParameterType = fieldClass.constructorParameterType;
if (constructorParameterType == char.class) {
Map<String, Character> constants = FIELD_CONSTANT_CACHE_INVERSE.get(fieldClass.klass);
return fieldClass.constructor.newInstance(constants.get(value));
}
if (constructorParameterType == int.class) {
return fieldClass.constructor.newInstance(Integer.valueOf(value));
}
if (constructorParameterType == String.class) {
return fieldClass.constructor.newInstance(value);
}
if (constructorParameterType == BigDecimal.class) {
return fieldClass.constructor.newInstance(new BigDecimal(value));
}
// TODO must be specialized with Field.class
if (constructorParameterType == Date.class) {
return fieldClass.constructor.newInstance(UtcTimestampConverter.convert(value));
}
throw new UnsupportedOperationException(constructorParameterType + " is not supported yet.");
}
private static void setField(Message msg, Field field) {
Class<?> fieldClass = field.getClass();
if (IntField.class.isAssignableFrom(fieldClass)) {
msg.setField((IntField) field);
} else if (StringField.class.isAssignableFrom(fieldClass)) {
msg.setField((StringField) field);
} else if (CharField.class.isAssignableFrom(fieldClass)) {
msg.setField((CharField) field);
} else if (DecimalField.class.isAssignableFrom(fieldClass)) {
msg.setField((DecimalField) field);
} else if (UtcTimeStampField.class.isAssignableFrom(fieldClass)) {
msg.setField((UtcTimeStampField) field);
} else {
throw new UnsupportedOperationException(fieldClass + " is not supported yet.");
}
}
private static Class<? extends Message> loadMessageClass(String beginString, String messageName) throws ClassNotFoundException {
String classKey = beginString + messageName;
Class<? extends Message> messageClass = MESSAGE_CLASS_CACHE.get(classKey);
if (messageClass == null) {
String fixVersion = beginString.replace(".", "").toLowerCase();
String packageName = "quickfix." + fixVersion + ".";
messageClass = (Class<Message>) Class.forName(packageName + messageName);
MESSAGE_CLASS_CACHE.putIfAbsent(classKey, messageClass);
}
return messageClass;
}
@Cached
private static FieldClass loadFieldClass(String fieldName) throws ClassNotFoundException {
FieldClass fieldClass = FIELD_CLASS_CACHE.get(fieldName);
if (fieldClass == null) {
Class<? extends Field> klass = (Class<? extends Field>) Class.forName("quickfix.field." + fieldName);
Constructor<? extends Field> constructor = (Constructor<? extends Field>) klass.getConstructors()[1];
Class<?> parameterType = constructor.getParameterTypes()[0];
fieldClass = new FieldClass(klass, constructor, parameterType);
FIELD_CLASS_CACHE.putIfAbsent(fieldName, fieldClass);
}
return fieldClass;
}
public static Message fromString(String str) throws Exception {
str = str.trim();
List<FieldEntry> fieldEntries = getMessageFieldEntries(str);
String beginString = fieldEntries.get(0).value;
int firstBracket = str.indexOf('{');
String messageName = str.substring(0, firstBracket);
Class<? extends Message> messageClass = loadMessageClass(beginString, messageName);
Message message = messageClass.newInstance();
// invokes cacheFieldConstants()
getFieldHelpers(messageClass);
for (FieldEntry fieldEntry : fieldEntries) {
FieldClass fieldClass = loadFieldClass(fieldEntry.name);
Field field = newFieldInstance(fieldClass, fieldEntry.value);
setField(message, field);
}
return message;
}
/**
* Demonstration.
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
NewOrderSingle newOrderSingle = new NewOrderSingle(new ClOrdID("0"), new HandlInst(HandlInst.AUTOMATED_EXECUTION_ORDER_PRIVATE), new Symbol("EUR/USD"), new Side(Side.BUY), new TransactTime(new Date()), new OrdType(OrdType.MARKET));
newOrderSingle.set(new Price(0.0));
System.out.println("Original: " + newOrderSingle.toString());
System.out.println("Readable: " + toString(newOrderSingle));
System.out.println(fromString(toString(newOrderSingle)));
System.out.println(toString(fromString(toString(newOrderSingle))));
String newOrderSingleStr = toString(newOrderSingle);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
fromString(newOrderSingleStr);
}
long end2 = System.currentTimeMillis();
System.out.println("fromString(): " + (end2 - start2) + "ms");
long start1 = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
newOrderSingle.toString();
}
long end1 = System.currentTimeMillis();
System.out.println(".toString(): " + (end1 - start1) + "ms");
long start = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
toString(newOrderSingle);
}
long end = System.currentTimeMillis();
System.out.println("toString(): " + (end - start) + "ms");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment