diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml
index 23dc556da7..8fbe442aa8 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/test/java/org/quickfixj/codegenerator/Compiler.java b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/Compiler.java
new file mode 100644
index 0000000000..2fd2c14643
--- /dev/null
+++ b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/Compiler.java
@@ -0,0 +1,73 @@
+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;
+ }
+ }
+}
\ No newline at end of file
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..f613a0ce37
--- /dev/null
+++ b/quickfixj-core/src/test/java/org/quickfixj/codegenerator/MessageCodeGeneratorTest.java
@@ -0,0 +1,148 @@
+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("/codegenerator/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("/codegenerator/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 extends Field>> clazz;
+ FieldDef(final int fieldNumber, final Class extends Field>> clazz) {
+ this.fieldNumber = fieldNumber;
+ this.clazz = clazz;
+ }
+ }
+ private final class MessageDef {
+ private final String messageType;
+ MessageDef(final String messageType) {
+ this.messageType = messageType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/quickfixj-core/src/test/resources/codegenerator/basic.xml b/quickfixj-core/src/test/resources/codegenerator/basic.xml
new file mode 100644
index 0000000000..13fb5e51ea
--- /dev/null
+++ b/quickfixj-core/src/test/resources/codegenerator/basic.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/quickfixj-core/src/test/resources/codegenerator/nested-group.xml b/quickfixj-core/src/test/resources/codegenerator/nested-group.xml
new file mode 100644
index 0000000000..c4cbbd6be1
--- /dev/null
+++ b/quickfixj-core/src/test/resources/codegenerator/nested-group.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file