/*******************************************************************************
* Copyright (c) quickfixengine.org All rights reserved.
*
* This file is part of the QuickFIX FIX Engine
*
* This file may be distributed under the terms of the quickfixengine.org
* license as defined by quickfixengine.org and appearing in the file
* LICENSE included in the packaging of this file.
*
* This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
* THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE.
*
* See http://www.quickfixengine.org/LICENSE for licensing information.
*
* Contact ask@quickfixengine.org if any conditions of this licensing
* are not clear to you.
******************************************************************************/
package quickfix.codegen;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Generates Message and Field related code for the various FIX versions.
*
*/
public class MessageCodeGenerator {
private static final String BIGDECIMAL_TYPE_OPTION = "generator.decimal";
private static final String ORDERED_FIELDS_OPTION = "generator.orderedFields";
private static final String OVERWRITE_OPTION = "generator.overwrite";
private Logger log = LoggerFactory.getLogger(getClass());
// An arbitrary serial UID which will have to be changed when messages and fields won't be compatible with next versions in terms
// of java serialization.
private static final long SERIAL_UID = 20050617;
// The String representation of the UID
private static final String SERIAL_UID_STR = String.valueOf(SERIAL_UID);
// The name of the param in the .xsl files to pass the serialVersionUID
private static final String XSLPARAM_SERIAL_UID = "serialVersionUID";
private void generateMessageBaseClass(Task task) throws TransformerConfigurationException,
FileNotFoundException, ParserConfigurationException, SAXException, IOException,
TransformerFactoryConfigurationError, TransformerException {
log.info(task.getName() + ": generating message base class");
Map<String, String> parameters = new HashMap<String, String>();
parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR);
generateClassCode(task, "Message", parameters);
}
private void generateMessageFactoryClass(Task task) throws TransformerConfigurationException,
FileNotFoundException, ParserConfigurationException, SAXException, IOException,
TransformerFactoryConfigurationError, TransformerException {
generateClassCode(task, "MessageFactory", null);
}
private void generateMessageCrackerClass(Task task) throws TransformerConfigurationException,
FileNotFoundException, ParserConfigurationException, SAXException, IOException,
TransformerFactoryConfigurationError, TransformerException {
generateClassCode(task, "MessageCracker", null);
}
private void generateClassCode(Task task, String className, Map<String, String> parameters)
throws ParserConfigurationException, SAXException, IOException,
TransformerFactoryConfigurationError, TransformerConfigurationException,
FileNotFoundException, TransformerException {
log.debug("generating " + className + " for " + task.getName());
if (parameters == null) {
parameters = new HashMap<String, String>();
}
parameters.put("messagePackage", task.getMessagePackage());
parameters.put("fieldPackage", task.getFieldPackage());
Document document = getSpecification(task);
generateCodeFile(task, document, parameters, task.getOutputBaseDirectory() + "/"
+ task.getMessageDirectory() + "/" + className + ".java", createTransformer(task,
className + ".xsl"));
}
private void generateFieldClasses(Task task) throws ParserConfigurationException, SAXException,
IOException {
log.info(task.getName() + ": generating field classes");
String outputDirectory = task.getOutputBaseDirectory() + "/" + task.getFieldDirectory()
+ "/";
writePackageDocumentation(outputDirectory, "FIX field definitions for " + task.getName());
Document document = getSpecification(task);
List<String> fieldNames = getNames(document.getDocumentElement(), "fields/field");
try {
Transformer transformer = createTransformer(task, "Fields.xsl");
for (int i = 0; i < fieldNames.size(); i++) {
String fieldName = fieldNames.get(i);
String outputFile = outputDirectory + fieldName + ".java";
if (!new File(outputFile).exists()) {
log.debug("field: " + fieldName);
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("fieldName", fieldName);
parameters.put("fieldPackage", task.getFieldPackage());
if (task.isDecimalGenerated()) {
parameters.put("decimalType", "java.math.BigDecimal");
parameters.put("decimalConverter", "Decimal");
}
parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR);
generateCodeFile(task, document, parameters, outputFile, transformer);
}
}
} catch (Exception e) {
log.error("error while generating field classes", e);
}
}
private void generateMessageSubclasses(Task task) throws ParserConfigurationException,
SAXException, IOException, TransformerConfigurationException, FileNotFoundException,
TransformerFactoryConfigurationError, TransformerException {
log.info(task.getName() + ": generating message subclasses");
String outputDirectory = task.getOutputBaseDirectory() + "/" + task.getMessageDirectory()
+ "/";
writePackageDocumentation(outputDirectory, "Message classes");
Document document = getSpecification(task);
List<String> messageNames = getNames(document.getDocumentElement(), "messages/message");
Transformer transformer = createTransformer(task, "MessageSubclass.xsl");
for (int i = 0; i < messageNames.size(); i++) {
String messageName = messageNames.get(i);
log.debug("generating message class: " + messageName);
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("itemName", messageName);
parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR);
parameters.put("orderedFields", Boolean.toString(task.isOrderedFields()));
parameters.put("fieldPackage", task.getFieldPackage());
parameters.put("messagePackage", task.getMessagePackage());
generateCodeFile(task, document, parameters, outputDirectory + messageName + ".java",
transformer);
}
}
private void generateComponentClasses(Task task) throws ParserConfigurationException,
SAXException, IOException, TransformerConfigurationException, FileNotFoundException,
TransformerFactoryConfigurationError, TransformerException {
log.info(task.getName() + ": generating component classes");
String outputDirectory = task.getOutputBaseDirectory() + "/" + task.getMessageDirectory()
+ "/component/";
Document document = getSpecification(task);
List<String> componentNames = getNames(document.getDocumentElement(),
"components/component");
if (componentNames.size() > 0) {
writePackageDocumentation(outputDirectory, "Message component classes");
}
Transformer transformer = createTransformer(task, "MessageSubclass.xsl");
for (int i = 0; i < componentNames.size(); i++) {
String componentName = componentNames.get(i);
log.debug("generating component class: " + componentName);
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("itemName", componentName);
parameters.put("baseClass", "quickfix.MessageComponent");
parameters.put("subpackage", ".component");
parameters.put("fieldPackage", task.getFieldPackage());
parameters.put("messagePackage", task.getMessagePackage());
parameters.put("orderedFields", Boolean.toString(task.isOrderedFields()));
parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR);
generateCodeFile(task, document, parameters, outputDirectory + componentName + ".java",
transformer);
}
}
private Transformer createTransformer(Task task, String xsltFile)
throws TransformerFactoryConfigurationError, TransformerConfigurationException {
StreamSource styleSource = new StreamSource(new File(task.getTransformDirectory() + "/"
+ xsltFile));
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer(styleSource);
return transformer;
}
private Map<String, Document> specificationCache = new HashMap<String, Document>();
private Document getSpecification(Task task) throws ParserConfigurationException, SAXException,
IOException {
Document document = specificationCache.get(task.getName());
if (document == null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(new File(task.getSpecification()));
specificationCache.put(task.getName(), document);
}
return document;
}
private void writePackageDocumentation(String outputDirectory, String description)
throws FileNotFoundException {
File packageDescription = new File(outputDirectory + "package.html");
File parentDirectory = packageDescription.getParentFile();
if (!parentDirectory.exists()) {
parentDirectory.mkdirs();
}
PrintStream out = new PrintStream(new FileOutputStream(packageDescription));
out.println("<html>");
out.println("<head><title/></head>");
out.println("<body>" + description + "</body>");
out.println("</html>");
out.close();
}
private List<String> getNames(Element element, String path) {
return getNames(element, path, new ArrayList<String>());
}
private List<String> getNames(Element element, String path, List<String> names) {
int separatorOffset = path.indexOf("/");
if (separatorOffset == -1) {
NodeList fieldNodeList = element.getElementsByTagName(path);
for (int i = 0; i < fieldNodeList.getLength(); i++) {
names.add(((Element) fieldNodeList.item(i)).getAttribute("name"));
}
} else {
String tag = path.substring(0, separatorOffset);
NodeList subnodes = element.getElementsByTagName(tag);
for (int i = 0; i < subnodes.getLength(); i++) {
getNames((Element) subnodes.item(i), path.substring(separatorOffset + 1), names);
}
}
return names;
}
private void generateCodeFile(Task task, Document document, Map<String, String> parameters,
String outputFileName, Transformer transformer)
throws TransformerFactoryConfigurationError, TransformerConfigurationException,
FileNotFoundException, TransformerException {
if (parameters != null) {
Iterator<Map.Entry<String, String>> paramItr = parameters.entrySet().iterator();
while (paramItr.hasNext()) {
Map.Entry<String, String> entry = paramItr.next();
transformer.setParameter((String) entry.getKey(), entry.getValue());
}
}
File out = new File(outputFileName);
if (!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
File outputFile = new File(outputFileName);
if (!task.isOverwrite() && outputFile.exists()) {
return;
}
DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(new FileOutputStream(outputFile));
transformer.transform(source, result);
}
/*
* Generate the Message and Field related source code.
*/
public void generate(Task task) {
try {
generateFieldClasses(task);
generateMessageBaseClass(task);
generateMessageFactoryClass(task);
generateMessageCrackerClass(task);
generateComponentClasses(task);
generateMessageSubclasses(task);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
public static class Task {
private String name;
private String specification;
private String outputBaseDirectory;
private String messagePackage;
private String fieldPackage;
private boolean overwrite = true;
private String transformDirectory;
private boolean orderedFields;
private boolean useDecimal;
public String getName() {
return name;
}
public void setOrderedFields(boolean orderedFields) {
this.orderedFields = orderedFields;
}
public boolean isOrderedFields() {
return orderedFields;
}
public String getTransformDirectory() {
return transformDirectory;
}
public void setName(String name) {
this.name = name;
}
public String getFieldPackage() {
return fieldPackage;
}
public String getFieldDirectory() {
return fieldPackage.replace('.', '/');
}
public void setFieldPackage(String fieldPackage) {
this.fieldPackage = fieldPackage;
}
public String getMessageDirectory() {
return messagePackage.replace('.', '/');
}
public String getMessagePackage() {
return messagePackage;
}
public void setMessagePackage(String messagePackage) {
this.messagePackage = messagePackage;
}
public String getOutputBaseDirectory() {
return outputBaseDirectory;
}
public void setOutputBaseDirectory(String outputBaseDirectory) {
this.outputBaseDirectory = outputBaseDirectory;
}
public String getSpecification() {
return specification;
}
public void setSpecification(String specification) {
this.specification = specification;
}
public boolean isOverwrite() {
return overwrite;
}
public void setOverwrite(boolean overwrite) {
this.overwrite = overwrite;
}
public void setTransformDirectory(String transformDirectory) {
this.transformDirectory = transformDirectory;
}
public void setDecimalGenerated(boolean useDecimal) {
this.useDecimal = useDecimal;
}
public boolean isDecimalGenerated() {
return useDecimal;
}
}
public static String stripSpaces(String str) {
StringBuffer b = new StringBuffer();
for (int i = 0; i < str.length(); ++i) {
char t = str.charAt(i);
if (t == ' ')
continue;
if (t == '.')
continue;
b.append(t);
}
return b.toString();
}
public static void main(String[] args) {
MessageCodeGenerator codeGenerator = new MessageCodeGenerator();
try {
if (args.length != 3) {
String classname = MessageCodeGenerator.class.getName();
System.err.println("usage: " + classname + " specDir xformDir outputBaseDir");
return;
}
boolean overwrite = getOption(OVERWRITE_OPTION, true);
boolean orderedFields = getOption(ORDERED_FIELDS_OPTION, false);
boolean useDecimal = getOption(BIGDECIMAL_TYPE_OPTION, false);
long start = System.currentTimeMillis();
final String[] vers = new String[] { "FIXT 1.1", "FIX 5.0", "FIX 4.4", "FIX 4.3", "FIX 4.2",
"FIX 4.1", "FIX 4.0" };
for (int i = 0; i < vers.length; ++i) {
Task task = new Task();
task.setName(vers[i]);
final String temp = stripSpaces(vers[i]);
task.setSpecification(args[0] + "/" + temp + ".xml");
task.setTransformDirectory(args[1]);
task.setMessagePackage("quickfix." + temp.toLowerCase());
task.setOutputBaseDirectory(args[2]);
task.setFieldPackage("quickfix.field");
task.setOverwrite(overwrite);
task.setOrderedFields(orderedFields);
task.setDecimalGenerated(useDecimal);
codeGenerator.generate(task);
}
double duration = System.currentTimeMillis() - start;
DecimalFormat durationFormat = new DecimalFormat("#.###");
codeGenerator.log.info("Time for generation: "
+ durationFormat.format(duration / 1000L) + " seconds");
} catch (Exception e) {
codeGenerator.log.error("error during code generation", e);
System.exit(1);
}
}
private static boolean getOption(String key, boolean defaultValue) {
return System.getProperties().containsKey(key) ? Boolean.getBoolean(key) : defaultValue;
}
}