uk.co.real_logic.artio.dictionary.generation.Generator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of artio-codecs Show documentation
Show all versions of artio-codecs Show documentation
High-Performance FIX Gateway
/*
* Copyright 2015-2024 Real Logic Limited., Monotonic Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package uk.co.real_logic.artio.dictionary.generation;
import org.agrona.AsciiSequenceView;
import org.agrona.MutableDirectBuffer;
import org.agrona.collections.IntHashSet;
import org.agrona.generation.OutputManager;
import uk.co.real_logic.artio.EncodingException;
import uk.co.real_logic.artio.dictionary.CharArraySet;
import uk.co.real_logic.artio.dictionary.CharArrayWrapper;
import uk.co.real_logic.artio.dictionary.SessionConstants;
import uk.co.real_logic.artio.dictionary.ir.*;
import uk.co.real_logic.artio.dictionary.ir.Dictionary;
import uk.co.real_logic.artio.dictionary.ir.Entry.Element;
import uk.co.real_logic.artio.fields.DecimalFloat;
import uk.co.real_logic.artio.fields.LocalMktDateEncoder;
import uk.co.real_logic.artio.fields.ReadOnlyDecimalFloat;
import uk.co.real_logic.artio.fields.UtcTimestampEncoder;
import uk.co.real_logic.artio.util.AsciiBuffer;
import uk.co.real_logic.artio.util.MutableAsciiBuffer;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import static java.util.regex.Pattern.MULTILINE;
import static java.util.stream.Collectors.joining;
import static uk.co.real_logic.artio.dictionary.generation.AggregateType.*;
import static uk.co.real_logic.artio.dictionary.generation.GenerationUtil.importFor;
import static uk.co.real_logic.artio.dictionary.generation.GenerationUtil.importStaticFor;
import static uk.co.real_logic.sbe.generation.java.JavaUtil.formatPropertyName;
public abstract class Generator
{
public static final String MSG_TYPE = "MsgType";
public static final String BEGIN_STRING = "BeginString";
public static final String BODY_LENGTH = "BodyLength";
public static final String CODEC_VALIDATION_ENABLED = "CODEC_VALIDATION_ENABLED";
public static final String CODEC_REJECT_UNKNOWN_FIELD_ENABLED = "CODEC_REJECT_UNKNOWN_FIELD_ENABLED";
public static final String RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY = "CODEC_REJECT_UNKNOWN_ENUM_VALUE_ENABLED";
public static final Pattern NEWLINE = Pattern.compile("^", MULTILINE);
public static final String MESSAGE_FIELDS = "messageFields";
protected String commonCompoundImports(final String form, final boolean headerWrapsTrailer,
final String messageFieldsSet)
{
final String headerParameter = headerWrapsTrailer ? "trailer" : "";
return String.format(
"%3$s" +
" private final Trailer%1$s trailer = new Trailer%1$s();\n\n" +
" public Trailer%1$s trailer()\n" +
" {\n" +
" return trailer;\n" +
" }\n\n" +
" private final Header%1$s header = new Header%1$s(%2$s);\n\n" +
" public Header%1$s header()\n" +
" {\n" +
" return header;\n" +
" }\n\n",
form,
headerParameter,
messageFieldsSet);
}
private static final String COMMON_COMPOUND_IMPORTS =
"import %1$s.Header%2$s;\n" +
"import %1$s.Trailer%2$s;\n";
protected final Dictionary dictionary;
protected final String thisPackage;
private final String commonPackage;
protected final OutputManager outputManager;
protected final Class> validationClass;
protected final Class> rejectUnknownFieldClass;
private final Class> rejectUnknownEnumValueClass;
protected final boolean flyweightsEnabled;
protected final String codecRejectUnknownEnumValueEnabled;
protected final String scope;
protected final boolean fixTagsInJavadoc;
protected final Deque aggregateStack = new ArrayDeque<>();
protected Generator(
final Dictionary dictionary,
final String thisPackage,
final String commonPackage,
final OutputManager outputManager,
final Class> validationClass,
final Class> rejectUnknownFieldClass,
final Class> rejectUnknownEnumValueClass,
final boolean flyweightsEnabled,
final String codecRejectUnknownEnumValueEnabled,
final boolean fixTagsInJavadoc)
{
this.dictionary = dictionary;
this.thisPackage = thisPackage;
this.commonPackage = commonPackage;
this.outputManager = outputManager;
this.validationClass = validationClass;
this.rejectUnknownFieldClass = rejectUnknownFieldClass;
this.rejectUnknownEnumValueClass = rejectUnknownEnumValueClass;
this.flyweightsEnabled = flyweightsEnabled;
this.codecRejectUnknownEnumValueEnabled = codecRejectUnknownEnumValueEnabled;
this.fixTagsInJavadoc = fixTagsInJavadoc;
scope = dictionary.shared() ? "protected" : "private";
}
public void generate()
{
generateAggregateFile(dictionary.header(), AggregateType.HEADER);
generateAggregateFile(dictionary.trailer(), AggregateType.TRAILER);
dictionary.components().forEach((name, component) -> generateAggregateFile(component, COMPONENT));
dictionary.messages().forEach((msg) -> generateAggregateFile(msg, MESSAGE));
}
protected abstract void generateAggregateFile(Aggregate aggregate, AggregateType type);
protected abstract Class> topType(AggregateType aggregateType);
protected void generateImports(
final String compoundSuffix,
final AggregateType type,
final Writer out,
final Class>... extraImports) throws IOException
{
out
.append(importFor(MutableDirectBuffer.class))
.append(importFor(AsciiSequenceView.class))
.append(importStaticFor(CodecUtil.class))
.append(importStaticFor(SessionConstants.class))
.append(importFor(topType(MESSAGE)));
if (topType(GROUP) != topType(MESSAGE))
{
out.append(importFor(topType(GROUP)));
}
out .append(type == MESSAGE ? String.format(COMMON_COMPOUND_IMPORTS, thisPackage, compoundSuffix) : "")
.append(importFor(ReadOnlyDecimalFloat.class))
.append(importFor(DecimalFloat.class))
.append(importFor(MutableAsciiBuffer.class))
.append(importFor(AsciiBuffer.class))
.append(importFor(LocalMktDateEncoder.class))
.append(importFor(UtcTimestampEncoder.class))
.append(importFor(StandardCharsets.class))
.append(importFor(Arrays.class))
.append(importFor(CharArraySet.class))
.append(importFor(IntHashSet.class))
.append(importFor(IntHashSet.IntIterator.class))
.append(importFor(EncodingException.class))
.append(importFor(CharArrayWrapper.class));
for (final Class> extraImport : extraImports)
{
out.append(importFor(extraImport));
}
out .append(importStaticFor(StandardCharsets.class, "US_ASCII"))
.append(importStaticFor(validationClass, CODEC_VALIDATION_ENABLED))
.append(importStaticFor(rejectUnknownFieldClass, CODEC_REJECT_UNKNOWN_FIELD_ENABLED))
.append(importStaticFor(rejectUnknownEnumValueClass, RUNTIME_REJECT_UNKNOWN_ENUM_VALUE_PROPERTY));
if (!thisPackage.equals(commonPackage) && !commonPackage.isEmpty())
{
out.append(importFor(commonPackage + ".*"));
}
if (hasParent())
{
out.append(importFor(parentDictCommonPackage() + ".*"));
}
}
protected String completeResetMethod(
final boolean isMessage,
final List entries,
final String additionalReset,
final boolean isInParent)
{
final StringBuilder methods = new StringBuilder();
final String resetEntries = resetEntries(entries, methods);
if (isMessage)
{
final String reset = isSharedParent() ? "" : String.format(
" public void reset()\n" +
" {\n" +
" header.reset();\n" +
" trailer.reset();\n" +
" resetMessage();\n" +
"%1$s" +
" }\n\n",
additionalReset);
final String resetParent = isInParent ? " super.resetMessage();\n" : "";
return String.format(
"%1$s" +
" public void resetMessage()\n" +
" {\n" +
"%4$s" +
"%2$s" +
" }\n\n" +
"%3$s",
reset,
resetEntries,
methods,
resetParent);
}
else
{
final String resetParent = isInParent ? " super.reset();\n" : "";
return String.format(
" public void reset()\n" +
" {\n" +
"%4$s" +
"%1$s" +
"%2$s" +
" }\n\n" +
"%3$s",
resetEntries,
isSharedParent() ? "" : additionalReset,
methods,
resetParent);
}
}
protected String resetEntries(final List entries, final StringBuilder methods)
{
return resetFields(entries, methods) +
resetComponents(entries, methods) +
resetGroups(entries, methods) +
resetAnyFields(entries, methods);
}
private String resetFields(final List entries, final StringBuilder methods)
{
return resetAllBy(
entries,
methods,
entry -> entry.isField() && !entry.isInParent(),
(entry) -> resetField(entry.required(), (Field)entry.element()),
this::callResetMethod);
}
protected String resetAllBy(
final List entries,
final StringBuilder methods,
final Predicate predicate,
final Function methodFactory,
final Function callFactory)
{
methods.append(entries
.stream()
.filter(predicate)
.map(methodFactory)
.collect(joining()));
return entries
.stream()
.filter(predicate)
.map(callFactory)
.collect(joining());
}
private String resetGroups(final List entries, final StringBuilder methods)
{
return resetAllBy(
entries,
methods,
Entry::isGroup,
this::resetGroup,
this::callResetMethod);
}
protected abstract String resetGroup(Entry entry);
protected abstract String resetAnyFields(List entries, StringBuilder methods);
private String resetField(final boolean isRequired, final Field field)
{
final String name = field.name();
if (isNotResettableField(name))
{
return "";
}
if (!isRequired)
{
return optionalReset(field, name);
}
switch (field.type())
{
case INT:
case LENGTH:
case SEQNUM:
case NUMINGROUP:
case DAYOFMONTH:
return resetRequiredInt(field);
case LONG:
return resetRequiredLong(field);
case FLOAT:
case PRICE:
case PRICEOFFSET:
case QTY:
case QUANTITY:
case PERCENTAGE:
case AMT:
return resetRequiredFloat(name);
case CHAR:
return resetFieldValue(field, "MISSING_CHAR");
case DATA:
case XMLDATA:
return resetFieldValue(field, "null");
case BOOLEAN:
return resetFieldValue(field, "false");
case STRING:
case MULTIPLEVALUESTRING:
case MULTIPLESTRINGVALUE:
case MULTIPLECHARVALUE:
case CURRENCY:
case EXCHANGE:
case COUNTRY:
case LANGUAGE:
return resetStringBasedData(name);
case UTCTIMESTAMP:
case LOCALMKTDATE:
case UTCTIMEONLY:
case UTCDATEONLY:
case MONTHYEAR:
case TZTIMEONLY:
case TZTIMESTAMP:
return resetTemporalValue(name);
default:
throw new IllegalArgumentException("Unknown type: " + field.type());
}
}
protected abstract String resetRequiredInt(Field field);
protected abstract String resetRequiredLong(Field field);
protected abstract String optionalReset(Field field, String name);
protected abstract String resetTemporalValue(String name);
protected abstract String resetComponents(List entries, StringBuilder methods);
protected abstract String resetStringBasedData(String name);
protected String nameOfResetMethod(final String name)
{
return "reset" + name;
}
private String callResetMethod(final Entry entry)
{
if (isNotResettableField(entry.name()))
{
return "";
}
return String.format(
" this.%1$s();\n",
nameOfResetMethod(entry.name()));
}
protected String hasField(final Entry entry)
{
final String name = entry.name();
return entry.required() ?
"" :
String.format(" %2$s boolean has%1$s;\n\n", name, scope);
}
protected String resetNothing(final String name)
{
return String.format(
" public void %1$s()\n" +
" {\n" +
" }\n\n",
nameOfResetMethod(name));
}
private boolean isNotResettableField(final String name)
{
return isDerivedField(name) || isPreCalculatedField(name);
}
private boolean isPreCalculatedField(final String name)
{
return "MessageName".equals(name) || BEGIN_STRING.equals(name) || MSG_TYPE.equals(name);
}
private boolean isDerivedField(final String name)
{
return isBodyLength(name) || isCheckSum(name);
}
protected abstract String resetRequiredFloat(String name);
protected abstract String resetLength(String name);
protected String resetByFlag(final String name)
{
return String.format(
" public void %2$s()\n" +
" {\n" +
" has%1$s = false;\n" +
" }\n\n",
name,
nameOfResetMethod(name));
}
protected String resetFieldValue(final Field field, final String resetValue)
{
final String name = field.name();
final boolean hasLengthField = field.type().hasLengthField(flyweightsEnabled);
final String lengthReset = hasLengthField ? " %2$sLength = 0;\n" : "";
return String.format(
" public void %1$s()\n" +
" {\n" +
lengthReset +
" %2$s = %3$s;\n" +
" }\n\n",
nameOfResetMethod(name),
formatPropertyName(name),
resetValue);
}
protected String generateAppendTo(final Aggregate aggregate, final boolean hasCommonCompounds)
{
final String entriesToString = aggregate
.entries()
.stream()
.map(this::generateEntryAppendTo)
.collect(joining("\n"));
final String prefix;
if (hasCommonCompounds && !isSharedParent())
{
prefix =
" builder.append(\" \\\"header\\\": \");\n" +
" header.appendTo(builder, level + 1);\n" +
" builder.append(\"\\n\");\n";
}
else
{
prefix = "";
}
return String.format(
" public String toString()\n" +
" {\n" +
" return appendTo(new StringBuilder()).toString();\n" +
" }\n\n" +
" public StringBuilder appendTo(final StringBuilder builder)\n" +
" {\n" +
" return appendTo(builder, 1);\n" +
" }\n\n" +
" public StringBuilder appendTo(final StringBuilder builder, final int level)\n" +
" {\n" +
" builder.append(\"{\\n\");" +
" indent(builder, level);\n" +
" builder.append(\"\\\"MessageName\\\": \\\"%1$s\\\",\\n\");\n" +
"%2$s" +
"%3$s" +
" indent(builder, level - 1);\n" +
" builder.append(\"}\");\n" +
" return builder;\n" +
" }\n\n",
aggregate.name(),
prefix,
entriesToString);
}
protected String generateEntryAppendTo(final Entry entry)
{
//" \"OnBehalfOfCompID\": \"abc\",\n" +
if (isBodyLength(entry))
{
return "";
}
final Element element = entry.element();
final String name = entry.name();
if (element instanceof Field)
{
final Field field = (Field)element;
final String value = fieldAppendTo(field);
final String fieldAppender = String.format(
" indent(builder, level);\n" +
" builder.append(\"\\\"%1$s\\\": \\\"\");\n" +
" %2$s;\n" +
" builder.append(\"\\\",\\n\");\n",
name,
value);
if (appendToChecksHasGetter(entry, field))
{
final String indentedFieldAppender = NEWLINE
.matcher(fieldAppender)
.replaceAll(" ");
return String.format(
" if (has%1$s())\n" +
" {\n" +
"%2$s" +
" }\n",
name,
indentedFieldAppender);
}
else
{
return fieldAppender;
}
}
else if (element instanceof Group)
{
return groupEntryAppendTo((Group)element, name);
}
else if (element instanceof Component)
{
return componentAppendTo((Component)element);
}
else if (element instanceof AnyFields)
{
return anyFieldsAppendTo((AnyFields)element);
}
return "";
}
protected abstract boolean appendToChecksHasGetter(Entry entry, Field field);
protected abstract String groupEntryAppendTo(Group element, String name);
protected abstract String anyFieldsAppendTo(AnyFields element);
protected abstract boolean hasFlag(Entry entry, Field field);
protected String hasGetter(final String name)
{
return String.format(
" public boolean has%s()\n" +
" {\n" +
" return has%1$s;\n" +
" }\n\n",
name);
}
protected abstract String componentAppendTo(Component component);
protected String fieldAppendTo(final Field field)
{
final String fieldName = formatPropertyName(field.name());
switch (field.type())
{
case STRING:
case MULTIPLEVALUESTRING:
case MULTIPLESTRINGVALUE:
case MULTIPLECHARVALUE:
case CURRENCY:
case EXCHANGE:
case COUNTRY:
case LANGUAGE:
return stringAppendTo(fieldName);
// Call the getter for other choices in order to ensure that the flyweight version is populated
case UTCTIMEONLY:
case UTCDATEONLY:
case UTCTIMESTAMP:
case LOCALMKTDATE:
case MONTHYEAR:
case TZTIMEONLY:
case TZTIMESTAMP:
return timeAppendTo(fieldName);
case FLOAT:
case PRICE:
case PRICEOFFSET:
case QTY:
case QUANTITY:
case PERCENTAGE:
case AMT:
if (flyweightsEnabled)
{
return String.format("this.%1$s().appendTo(builder)", fieldName);
}
return String.format("%1$s.appendTo(builder)", fieldName);
case DATA:
case XMLDATA:
return dataAppendTo(field, fieldName);
default:
if (flyweightsEnabled)
{
return String.format("builder.append(this.%1$s())", fieldName);
}
return String.format("builder.append(%1$s)", fieldName);
}
}
protected abstract String timeAppendTo(String fieldName);
protected abstract String dataAppendTo(Field field, String fieldName);
protected boolean isCheckSum(final Entry entry)
{
return entry != null && isCheckSum(entry.name());
}
private boolean isCheckSum(final String name)
{
return "CheckSum".equals(name);
}
protected boolean isBodyLength(final Entry entry)
{
return entry != null && isBodyLength(entry.name());
}
protected boolean isBeginString(final Entry entry)
{
return entry != null && BEGIN_STRING.equals(entry.name());
}
protected boolean isBodyLength(final String name)
{
return BODY_LENGTH.equals(name);
}
void generateOptionalSessionFieldsSupportedMethods(
final List optionalFields, final Set missingOptionalFields, final Writer out)
throws IOException
{
if (optionalFields != null)
{
for (final String optionalField : optionalFields)
{
final boolean inDictionary = !missingOptionalFields.contains(optionalField);
out.append(String.format(
" public boolean supports%1$s()\n" +
" {\n" +
" return %2$s;\n" +
" }\n\n",
optionalField,
inDictionary));
}
}
}
boolean isSharedParent()
{
return dictionary.shared();
}
boolean hasParent()
{
return dictionary.hasSharedParent();
}
String parentDictPackage()
{
return toParentDictPackage(thisPackage);
}
String parentDictCommonPackage()
{
return toParentDictPackage(commonPackage);
}
private String toParentDictPackage(final String whichPackage)
{
return whichPackage.replace("." + dictionary.name(), "");
}
protected abstract String stringAppendTo(String fieldName);
protected String indent(final int times, final String suffix)
{
final StringBuilder sb = new StringBuilder(times * 4 + suffix.length());
IntStream.range(0, times).forEach((ignore) -> sb.append(" "));
sb.append(suffix);
return sb.toString();
}
boolean shouldGenerateClassEnumMethods(final Field field)
{
return EnumGenerator.hasEnumGenerated(field) && !field.type().isMultiValue() &&
!field.hasSharedSometimesEnumClash();
}
Aggregate currentAggregate()
{
return aggregateStack.peekLast();
}
Aggregate parentAggregate()
{
final Aggregate current = aggregateStack.removeLast();
final Aggregate parent = aggregateStack.peekLast();
push(current);
return parent;
}
void pop()
{
aggregateStack.removeLast();
}
void push(final Aggregate aggregate)
{
aggregateStack.addLast(aggregate);
}
String qualifiedAggregateStackNames(final Function toName)
{
return aggregateStack.stream().map(toName).collect(joining("."));
}
protected String generateAccessorJavadoc(final Field field)
{
if (fixTagsInJavadoc)
{
return "/* " + field.name() + " = " + field.number() + " */\n ";
}
else
{
return "";
}
}
}