Last active
December 16, 2017 20:27
-
-
Save bonifaido/5141859 to your computer and use it in GitHub Desktop.
This utility class converts QuickFIX/J Messages into a human readable format.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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