diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml index 854485eb7e..f2f7177ac2 100644 --- a/quickfixj-core/pom.xml +++ b/quickfixj-core/pom.xml @@ -56,6 +56,19 @@ test + + org.quickfixj + quickfixj-codegenerator + ${project.version} + test + + + org.jooq + joor-java-8 + 0.9.9 + test + + org.apache.mina mina-core diff --git a/quickfixj-core/src/main/java/quickfix/DataDictionary.java b/quickfixj-core/src/main/java/quickfix/DataDictionary.java index 7c86c22116..6e6c51b7c9 100644 --- a/quickfixj-core/src/main/java/quickfix/DataDictionary.java +++ b/quickfixj-core/src/main/java/quickfix/DataDictionary.java @@ -48,10 +48,7 @@ import java.util.Map; import java.util.Set; -import static quickfix.FileUtil.Location.CLASSLOADER_RESOURCE; -import static quickfix.FileUtil.Location.CONTEXT_RESOURCE; -import static quickfix.FileUtil.Location.FILESYSTEM; -import static quickfix.FileUtil.Location.URL; +import static quickfix.FileUtil.Location.*; /** * Provide the message metadata for various versions of FIX. @@ -697,6 +694,9 @@ private void checkValidFormat(StringField field) throws IncorrectDataFormat { if (fieldType == null) { return; } + if (field.getValue().length() == 0 && !checkFieldsHaveValues) { + return; + } try { switch (fieldType) { case STRING: diff --git a/quickfixj-core/src/test/java/org/quickfixj/codegenerator/Compiler.java b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/Compiler.java new file mode 100644 index 0000000000..7b73013f7b --- /dev/null +++ b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/Compiler.java @@ -0,0 +1,92 @@ +package org.quickfixj.codegenerator; + +import org.joor.Reflect; + +import javax.tools.*; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +class Compiler { + private Compiler() { + } + + static Map compile(final Map classNameToSourceMap) { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + final ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null)); + + final List files = new ArrayList<>(); + for (final Map.Entry entry : classNameToSourceMap.entrySet()) { + files.add(new CharSequenceJavaFileObject(entry.getKey(), entry.getValue())); + } + + final StringWriter out = new StringWriter(); + compiler.getTask(out, fileManager, null, null, null, files).call(); + + if (!fileManager.output.keySet().containsAll(classNameToSourceMap.keySet())) { + throw new RuntimeException("Compilation error:\n" + out.toString()); + } + + final ClassLoader cl = lookup.lookupClass().getClassLoader(); + final Map instances = new LinkedHashMap<>(); + for (final Map.Entry output : fileManager.output.entrySet()) { + final String className = output.getKey(); + final byte[] b = output.getValue().getBytes(); + final Class clazz = Reflect.on(cl).call("defineClass", className, b, 0, b.length).get(); + instances.put(className, Reflect.on(clazz)); + } + return instances; + } + + private static final class ClassFileManager extends ForwardingJavaFileManager { + private final Map output = new LinkedHashMap<>(); + + ClassFileManager(final StandardJavaFileManager standardManager) { + super(standardManager); + } + + @Override + public JavaFileObject getJavaFileForOutput(final JavaFileManager.Location location, final String className, final JavaFileObject.Kind kind, final FileObject sibling) { + return output.computeIfAbsent(className, (cn) -> new JavaFileObject(cn, kind)); + } + } + + private static final class JavaFileObject extends SimpleJavaFileObject { + private final ByteArrayOutputStream os = new ByteArrayOutputStream(); + + JavaFileObject(final String name, final JavaFileObject.Kind kind) { + super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind); + } + + byte[] getBytes() { + return os.toByteArray(); + } + + @Override + public OutputStream openOutputStream() { + return os; + } + } + + private static final class CharSequenceJavaFileObject extends SimpleJavaFileObject { + private final CharSequence content; + + CharSequenceJavaFileObject(final String className, final CharSequence content) { + super(URI.create("string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE); + this.content = content; + } + + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) { + return content; + } + } +} diff --git a/quickfixj-core/src/test/java/org/quickfixj/codegenerator/MessageCodeGeneratorTest.java b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/MessageCodeGeneratorTest.java new file mode 100644 index 0000000000..b26c27b433 --- /dev/null +++ b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/MessageCodeGeneratorTest.java @@ -0,0 +1,173 @@ +package org.quickfixj.codegenerator; + +import org.joor.Reflect; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import quickfix.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MessageCodeGeneratorTest { + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void generateFromBasicFixDictionary() throws Exception { + final File schema = new File(MessageCodeGeneratorTest.class.getResource("/org/quickfixj/codegenerator/MessageFactory.xsl").getFile()); + final File spec = new File(MessageCodeGeneratorTest.class.getResource("/basic.xml").getFile()); + + final MessageCodeGenerator.Task task = new MessageCodeGenerator.Task(); + task.setName("basic"); + task.setSpecification(spec); + task.setTransformDirectory(schema.getParentFile()); + task.setMessagePackage("basic"); + task.setOutputBaseDirectory(folder.getRoot()); + task.setFieldPackage("field"); + task.setOverwrite(true); + task.setOrderedFields(true); + task.setDecimalGenerated(false); + final MessageCodeGenerator generator = new MessageCodeGenerator(); + generator.generate(task); + + final File fieldDir = new File(folder.getRoot(), "field"); + final File messageDir = new File(folder.getRoot(), "basic"); + + final Map classNameToSourceMap = new LinkedHashMap<>(); + classNameToSourceMap.put("field.BeginString", getSource(new File(fieldDir, "BeginString.java"))); + classNameToSourceMap.put("field.BodyLength", getSource(new File(fieldDir, "BodyLength.java"))); + classNameToSourceMap.put("field.CheckSum", getSource(new File(fieldDir, "CheckSum.java"))); + classNameToSourceMap.put("field.MsgType", getSource(new File(fieldDir, "MsgType.java"))); + classNameToSourceMap.put("field.Signature", getSource(new File(fieldDir, "Signature.java"))); + classNameToSourceMap.put("field.SignatureLength", getSource(new File(fieldDir, "SignatureLength.java"))); + classNameToSourceMap.put("field.TestReqID", getSource(new File(fieldDir, "TestReqID.java"))); + + classNameToSourceMap.put("basic.Message", getSource(new File(messageDir, "Message.java"))); + classNameToSourceMap.put("basic.TestRequest", getSource(new File(messageDir, "TestRequest.java"))); + classNameToSourceMap.put("basic.MessageCracker", getSource(new File(messageDir, "MessageCracker.java"))); + classNameToSourceMap.put("basic.MessageFactory", getSource(new File(messageDir, "MessageFactory.java"))); + final Map classes = Compiler.compile(classNameToSourceMap); + + final Map fieldDefs = new LinkedHashMap<>(); + fieldDefs.put("BeginString", new FieldDef(8, StringField.class)); + fieldDefs.put("BodyLength", new FieldDef(9, IntField.class)); + fieldDefs.put("CheckSum", new FieldDef(10, StringField.class)); + fieldDefs.put("MsgType", new FieldDef(35, StringField.class)); + fieldDefs.put("Signature", new FieldDef(89, StringField.class)); + fieldDefs.put("SignatureLength", new FieldDef(93, IntField.class)); + fieldDefs.put("TestReqID", new FieldDef(112, StringField.class)); + validateFields(classes, fieldDefs); + + final Map messageDefs = new LinkedHashMap<>(); + messageDefs.put("TestRequest", new MessageDef("1")); + validateMessages(classes, messageDefs); + } + + /** + * This test is based on the FXAll FIX spec post MiFID II which has the same group in different locations within a + * message based on the context of the message. At present this generates Java code which does not compile due to + * duplicate case labels. + */ + @Test(expected = RuntimeException.class) + public void generateFromFixDictionaryWithNestedGroups() throws Exception { + final File schema = new File(MessageCodeGeneratorTest.class.getResource("/org/quickfixj/codegenerator/MessageFactory.xsl").getFile()); + final File spec = new File(MessageCodeGeneratorTest.class.getResource("/nested-group.xml").getFile()); + + final MessageCodeGenerator.Task task = new MessageCodeGenerator.Task(); + task.setName("nested"); + task.setSpecification(spec); + task.setTransformDirectory(schema.getParentFile()); + task.setMessagePackage("nested"); + task.setOutputBaseDirectory(folder.getRoot()); + task.setFieldPackage("field"); + task.setOverwrite(true); + task.setOrderedFields(true); + task.setDecimalGenerated(false); + final MessageCodeGenerator generator = new MessageCodeGenerator(); + generator.generate(task); + + final File fieldDir = new File(folder.getRoot(), "field"); + final File messageDir = new File(folder.getRoot(), "nested"); + + final Map classNameToSourceMap = new LinkedHashMap<>(); + classNameToSourceMap.put("field.BeginString", getSource(new File(fieldDir, "BeginString.java"))); + classNameToSourceMap.put("field.BodyLength", getSource(new File(fieldDir, "BodyLength.java"))); + classNameToSourceMap.put("field.CheckSum", getSource(new File(fieldDir, "CheckSum.java"))); + classNameToSourceMap.put("field.MsgType", getSource(new File(fieldDir, "MsgType.java"))); + classNameToSourceMap.put("field.Signature", getSource(new File(fieldDir, "Signature.java"))); + classNameToSourceMap.put("field.SignatureLength", getSource(new File(fieldDir, "SignatureLength.java"))); + classNameToSourceMap.put("field.TestReqID", getSource(new File(fieldDir, "TestReqID.java"))); + classNameToSourceMap.put("field.NoFoos", getSource(new File(fieldDir, "NoFoos.java"))); + classNameToSourceMap.put("field.NoBars", getSource(new File(fieldDir, "NoBars.java"))); + classNameToSourceMap.put("field.Foo", getSource(new File(fieldDir, "Foo.java"))); + + classNameToSourceMap.put("basic.Message", getSource(new File(messageDir, "Message.java"))); + classNameToSourceMap.put("basic.TestRequest", getSource(new File(messageDir, "TestRequest.java"))); + classNameToSourceMap.put("basic.MessageCracker", getSource(new File(messageDir, "MessageCracker.java"))); + classNameToSourceMap.put("basic.MessageFactory", getSource(new File(messageDir, "MessageFactory.java"))); + final Map classes = Compiler.compile(classNameToSourceMap); + + final Map fieldDefs = new LinkedHashMap<>(); + fieldDefs.put("BeginString", new FieldDef(8, StringField.class)); + fieldDefs.put("BodyLength", new FieldDef(9, IntField.class)); + fieldDefs.put("CheckSum", new FieldDef(10, StringField.class)); + fieldDefs.put("MsgType", new FieldDef(35, StringField.class)); + fieldDefs.put("Signature", new FieldDef(89, StringField.class)); + fieldDefs.put("SignatureLength", new FieldDef(93, IntField.class)); + fieldDefs.put("TestReqID", new FieldDef(112, StringField.class)); + fieldDefs.put("NoFoos", new FieldDef(112, IntField.class)); + fieldDefs.put("NoBars", new FieldDef(112, IntField.class)); + fieldDefs.put("Foo", new FieldDef(112, StringField.class)); + validateFields(classes, fieldDefs); + + final Map messageDefs = new LinkedHashMap<>(); + messageDefs.put("TestRequest", new MessageDef("1")); + validateMessages(classes, messageDefs); + } + + private String getSource(final File file) throws IOException { + return new String(Files.readAllBytes(file.toPath())); + } + + private void validateFields(final Map classes, final Map fieldDefs) { + for (final Map.Entry fieldDef : fieldDefs.entrySet()) { + final String fieldName = fieldDef.getKey(); + final Field fieldInstance = classes.get("field." + fieldName).create().get(); + assertEquals(String.format("Mismatch on field number for %s", fieldName), fieldDef.getValue().fieldNumber, fieldInstance.getField()); + assertTrue(String.format("Expected %s to be an instance of %s", fieldName, fieldDef.getValue().clazz.getSimpleName()), fieldDef.getValue().clazz.isAssignableFrom(fieldInstance.getClass())); + } + } + + private void validateMessages(final Map classes, final Map messageDefs) throws FieldNotFound { + for (final Map.Entry messageDef : messageDefs.entrySet()) { + final String messageName = messageDef.getKey(); + final Message messageInstance = classes.get("basic." + messageName).create().get(); + assertEquals(String.format("Mismatch on message type for %s", messageName), messageDef.getValue().messageType, messageInstance.getHeader().getString(35)); + } + } + + private final class FieldDef { + private final int fieldNumber; + private final Class> clazz; + + FieldDef(final int fieldNumber, final Class> clazz) { + this.fieldNumber = fieldNumber; + this.clazz = clazz; + } + } + + private final class MessageDef { + private final String messageType; + + MessageDef(final String messageType) { + this.messageType = messageType; + } + } +} diff --git a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java index 0f9d11b4dc..7313d279b9 100644 --- a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java @@ -22,31 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import quickfix.field.Account; -import quickfix.field.AvgPx; -import quickfix.field.BodyLength; -import quickfix.field.CheckSum; -import quickfix.field.ClOrdID; -import quickfix.field.HandlInst; -import quickfix.field.LastMkt; -import quickfix.field.MsgSeqNum; -import quickfix.field.MsgType; -import quickfix.field.NoHops; -import quickfix.field.NoPartyIDs; -import quickfix.field.NoRelatedSym; -import quickfix.field.OrdType; -import quickfix.field.OrderQty; -import quickfix.field.Price; -import quickfix.field.QuoteReqID; -import quickfix.field.SenderCompID; -import quickfix.field.SenderSubID; -import quickfix.field.SendingTime; -import quickfix.field.SessionRejectReason; -import quickfix.field.Side; -import quickfix.field.Symbol; -import quickfix.field.TargetCompID; -import quickfix.field.TimeInForce; -import quickfix.field.TransactTime; +import quickfix.field.*; import quickfix.fix44.NewOrderSingle; import quickfix.test.util.ExpectedTestFailure; @@ -57,10 +33,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasProperty; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class DataDictionaryTest { @@ -1275,6 +1248,30 @@ public void testGroupWithReqdComponentWithReqdFieldValidation() throws Exception dictionary.validate(quoteRequest, true); } + /** + * Field EffectiveTime(168) is defined as UTCTIMESTAMP so an empty string value is invalid but if we allow blank values that should not fail + * validation + * @throws Exception + */ + @Test + public void testAllowingBlankValuesDisablesFieldValidation() throws Exception { + final DataDictionary dictionary = getDictionary(); + dictionary.setCheckFieldsHaveValues(false); + + final quickfix.fix44.NewOrderSingle newSingle = new quickfix.fix44.NewOrderSingle( + new ClOrdID("123"), new Side(Side.BUY), new TransactTime(), new OrdType(OrdType.LIMIT) + ); + newSingle.setField(new OrderQty(42)); + newSingle.setField(new Price(42.37)); + newSingle.setField(new HandlInst()); + newSingle.setField(new Symbol("QFJ")); + newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER_BEST_EXECUTION)); + newSingle.setField(new TimeInForce(TimeInForce.DAY)); + newSingle.setField(new Account("testAccount")); + newSingle.setField(new StringField(EffectiveTime.FIELD)); + dictionary.validate(newSingle, true); + } + // // Group Validation Tests in RepeatingGroupTest // diff --git a/quickfixj-core/src/test/resources/basic.xml b/quickfixj-core/src/test/resources/basic.xml new file mode 100644 index 0000000000..4482e9b8f7 --- /dev/null +++ b/quickfixj-core/src/test/resources/basic.xml @@ -0,0 +1,120 @@ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-core/src/test/resources/nested-group.xml b/quickfixj-core/src/test/resources/nested-group.xml new file mode 100644 index 0000000000..720334cbda --- /dev/null +++ b/quickfixj-core/src/test/resources/nested-group.xml @@ -0,0 +1,131 @@ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +