diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java index 92a39844f5..02ae2a938f 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpGenerator.java @@ -38,6 +38,7 @@ import static uk.co.real_logic.sbe.ir.GenerationUtil.collectVarData; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups; import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields; +import static uk.co.real_logic.sbe.ir.GenerationUtil.findEndSignal; /** * Codec generator for the CSharp programming language. @@ -47,6 +48,8 @@ public class CSharpGenerator implements CodeGenerator { private static final String META_ATTRIBUTE_ENUM = "MetaAttribute"; private static final String INDENT = " "; + private static final String TWO_INDENT = INDENT + INDENT; + private static final String THREE_INDENT = INDENT + INDENT + INDENT; private static final String BASE_INDENT = INDENT; private final Ir ir; @@ -142,6 +145,8 @@ public void generate() throws IOException collectVarData(messageBody, offset, varData); out.append(generateVarData(varData, BASE_INDENT + INDENT)); + out.append(generateDisplay(toUpperFirstChar(msgToken.name()), fields, groups, varData)); + out.append(INDENT + "}\n"); out.append("}\n"); } @@ -180,6 +185,8 @@ private void generateGroups( i = collectVarData(tokens, i, varData); sb.append(generateVarData(varData, indent + INDENT + INDENT)); + appendGroupInstanceDisplay(sb, fields, groups, varData, indent + TWO_INDENT); + sb.append(indent).append(INDENT + "}\n"); } } @@ -481,29 +488,32 @@ private CharSequence generateVarData(final List tokens, final String inde lengthCSharpType, byteOrderStr)); - sb.append(lineSeparator()) - .append(String.format( - indent + "public string Get%1$s()\n" + - indent + "{\n" + - indent + INDENT + "const int sizeOfLengthField = %2$d;\n" + - indent + INDENT + "int limit = _parentMessage.Limit;\n" + - indent + INDENT + "_buffer.CheckLimit(limit + sizeOfLengthField);\n" + - indent + INDENT + "int dataLength = (int)_buffer.%3$sGet%4$s(limit);\n" + - indent + INDENT + "_parentMessage.Limit = limit + sizeOfLengthField + dataLength;\n" + - indent + INDENT + "return _buffer.GetStringFromBytes(%1$sResolvedCharacterEncoding," + - " limit + sizeOfLengthField, dataLength);\n" + - indent + "}\n\n" + - indent + "public void Set%1$s(string value)\n" + - indent + "{\n" + - indent + INDENT + "var encoding = %1$sResolvedCharacterEncoding;\n" + - indent + INDENT + "const int sizeOfLengthField = %2$d;\n" + - indent + INDENT + "int limit = _parentMessage.Limit;\n" + - indent + INDENT + "int byteCount = _buffer.SetBytesFromString(encoding, value, " + - "limit + sizeOfLengthField);\n" + - indent + INDENT + "_parentMessage.Limit = limit + sizeOfLengthField + byteCount;\n" + - indent + INDENT + "_buffer.%3$sPut%4$s(limit, (ushort)byteCount);\n" + - indent + "}\n", - propertyName, sizeOfLengthField, lengthTypePrefix, byteOrderStr)); + if (characterEncoding != null) // only generate these string based methods if there is an encoding + { + sb.append(lineSeparator()) + .append(String.format( + indent + "public string Get%1$s()\n" + + indent + "{\n" + + indent + INDENT + "const int sizeOfLengthField = %2$d;\n" + + indent + INDENT + "int limit = _parentMessage.Limit;\n" + + indent + INDENT + "_buffer.CheckLimit(limit + sizeOfLengthField);\n" + + indent + INDENT + "int dataLength = (int)_buffer.%3$sGet%4$s(limit);\n" + + indent + INDENT + "_parentMessage.Limit = limit + sizeOfLengthField + dataLength;\n" + + indent + INDENT + "return _buffer.GetStringFromBytes(%1$sResolvedCharacterEncoding," + + " limit + sizeOfLengthField, dataLength);\n" + + indent + "}\n\n" + + indent + "public void Set%1$s(string value)\n" + + indent + "{\n" + + indent + INDENT + "var encoding = %1$sResolvedCharacterEncoding;\n" + + indent + INDENT + "const int sizeOfLengthField = %2$d;\n" + + indent + INDENT + "int limit = _parentMessage.Limit;\n" + + indent + INDENT + "int byteCount = _buffer.SetBytesFromString(encoding, value, " + + "limit + sizeOfLengthField);\n" + + indent + INDENT + "_parentMessage.Limit = limit + sizeOfLengthField + byteCount;\n" + + indent + INDENT + "_buffer.%3$sPut%4$s(limit, (ushort)byteCount);\n" + + indent + "}\n", + propertyName, sizeOfLengthField, lengthTypePrefix, byteOrderStr)); + } } } @@ -525,6 +535,7 @@ private void generateBitSet(final List tokens) throws IOException out.append(generateChoices(tokens.subList(1, tokens.size() - 1))); out.append(INDENT + "}\n"); + out.append(generateChoiceDisplay(enumName)); out.append("}\n"); } } @@ -560,6 +571,7 @@ private void generateComposite(final List tokens) throws IOException out.append(generateFixedFlyweightCode(tokens.get(0).encodedLength())); out.append(generateCompositePropertyElements(tokens.subList(1, tokens.size() - 1), BASE_INDENT)); + out.append(generateCompositeDisplay(tokens)); out.append(INDENT + "}\n"); out.append("}\n"); } @@ -1029,12 +1041,14 @@ private void generateCharacterEncodingMethod( final String encoding, final String indent) { - sb.append(String.format("\n" + - indent + "public const string %1$sCharacterEncoding = \"%2$s\";\n" + - indent + "public static Encoding %1$sResolvedCharacterEncoding = " + - "Encoding.GetEncoding(%1$sCharacterEncoding);\n\n", - formatPropertyName(propertyName), - encoding)); + if (encoding != null) // Raw data fields might not have encodings + { + sb.append(String.format("\n" + + indent + "public const string %1$sCharacterEncoding = \"%2$s\";\n" + + indent + "public static Encoding %1$sResolvedCharacterEncoding = " + + "Encoding.GetEncoding(%1$sCharacterEncoding);\n\n", + formatPropertyName(propertyName), encoding)); + } } private CharSequence generateConstPropertyMethods( @@ -1540,4 +1554,317 @@ private String generateLiteral(final PrimitiveType type, final String value) return literal; } + + private void appendGroupInstanceDisplay( + final StringBuilder sb, + final List fields, + final List groups, + final List varData, + final String baseIndent) + { + final String indent = baseIndent + INDENT; + + sb.append('\n'); + append(sb, indent, "internal void BuildString(StringBuilder builder)"); + append(sb, indent, "{"); + append(sb, indent, " if (_buffer == null)"); + append(sb, indent, " {"); + append(sb, indent, " return;"); + append(sb, indent, " }"); + sb.append('\n'); + Separators.BEGIN_COMPOSITE.appendToGeneratedBuilder(sb, indent + INDENT, "builder"); + appendDisplay(sb, fields, groups, varData, indent + INDENT); + Separators.END_COMPOSITE.appendToGeneratedBuilder(sb, indent + INDENT, "builder"); + sb.append('\n'); + append(sb, indent, "}"); + } + + private void appendDisplay( + final StringBuilder sb, + final List fields, + final List groups, + final List varData, + final String indent) + { + int lengthBeforeLastGeneratedSeparator = -1; + + for (int i = 0, size = fields.size(); i < size;) + { + final Token fieldToken = fields.get(i); + if (fieldToken.signal() == Signal.BEGIN_FIELD) + { + final Token encodingToken = fields.get(i + 1); + final String fieldName = formatPropertyName(fieldToken.name()); + lengthBeforeLastGeneratedSeparator = writeTokenDisplay(fieldName, encodingToken, sb, indent); + + i += fieldToken.componentTokenCount(); + } + else + { + i++; + } + } + + for (int i = 0, size = groups.size(); i < size; i++) + { + final Token groupToken = groups.get(i); + if (groupToken.signal() != Signal.BEGIN_GROUP) + { + throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + groupToken); + } + + final String groupName = formatPropertyName(groupToken.name()); + final String varName = formatVariableName(groupToken.name()); + + append(sb, indent, "builder.Append(\"" + groupName + Separators.KEY_VALUE + + Separators.BEGIN_GROUP + "\");"); + append(sb, indent, "var " + varName + " = this." + groupName + ";"); + append(sb, indent, "if (" + varName + ".Count > 0)"); + append(sb, indent, "{"); + append(sb, indent, " var first = true;"); + append(sb, indent, " while (" + varName + ".HasNext)"); + append(sb, indent, " {"); + append(sb, indent, " if (!first)"); + append(sb, indent, " {"); + append(sb, indent, " builder.Append(',');"); + append(sb, indent, " }"); + append(sb, indent, " first = false;"); + append(sb, indent, " " + varName + ".Next().BuildString(builder);"); + append(sb, indent, " }"); + append(sb, indent, "}"); + append(sb, indent, "builder.Append(\"" + Separators.END_GROUP + "\");"); + + lengthBeforeLastGeneratedSeparator = sb.length(); + Separators.FIELD.appendToGeneratedBuilder(sb, indent, "builder"); + + i = findEndSignal(groups, i, Signal.END_GROUP, groupToken.name()); + } + + for (int i = 0, size = varData.size(); i < size;) + { + final Token lengthToken = Generators.findFirst("length", varData, i); + final int sizeOfLengthField = lengthToken.encodedLength(); + final Token varDataToken = varData.get(i); + if (varDataToken.signal() != Signal.BEGIN_VAR_DATA) + { + throw new IllegalStateException("tokens must begin with BEGIN_VAR_DATA: token=" + varDataToken); + } + + final String characterEncoding = varData.get(i + 3).encoding().characterEncoding(); + final String varDataName = formatPropertyName(varDataToken.name()); + final String getterName = formatGetterName(varDataToken.name()); + append(sb, indent, "builder.Append(\"" + varDataName + Separators.KEY_VALUE + "\");"); + if (characterEncoding == null) + { + final String name = Generators.toUpperFirstChar(varDataToken.name()); + append(sb, indent, "builder.Append(" + name + "Length()).Append(\" bytes of raw data\");"); + append(sb, indent, "_parentMessage.Limit = _parentMessage.Limit + " + + sizeOfLengthField + " + " + name + "Length();\n"); + } + else + { + append(sb, indent, "builder.Append('\\'').Append(" + getterName + "()).Append('\\'');"); + } + + lengthBeforeLastGeneratedSeparator = sb.length(); + Separators.FIELD.appendToGeneratedBuilder(sb, indent, "builder"); + + i += varDataToken.componentTokenCount(); + } + + if (lengthBeforeLastGeneratedSeparator > -1) + { + sb.setLength(lengthBeforeLastGeneratedSeparator); + } + } + + private int writeTokenDisplay( + final String fieldName, + final Token typeToken, + final StringBuilder sb, + final String indent) + { + if (typeToken.encodedLength() <= 0) + { + return -1; + } + + append(sb, indent, "builder.Append(\"" + fieldName + Separators.KEY_VALUE + "\");"); + + if (typeToken.isConstantEncoding()) + { + append(sb, indent, "builder.Append(\"" + typeToken.encoding().constValue() + "\");"); + } + else + { + switch (typeToken.signal()) + { + case ENCODING: + if (typeToken.arrayLength() > 1) + { + if (typeToken.encoding().primitiveType() == PrimitiveType.CHAR) + { + append(sb, indent, "for (int i = 0; i < " + fieldName + + "Length && this.Get" + fieldName + "(i) > 0; ++i)"); + append(sb, indent, "{"); + append(sb, indent, " builder.Append((char)this.Get" + fieldName + "(i));"); + append(sb, indent, "}"); + } + else + { + Separators.BEGIN_ARRAY.appendToGeneratedBuilder(sb, indent, "builder"); + append(sb, indent, "for (int i = 0; i < " + fieldName + "Length; ++i)"); + append(sb, indent, "{"); + append(sb, indent, " if (i > 0) builder.Append(',');"); + append(sb, indent, " builder.Append(Get" + fieldName + "(i));"); + append(sb, indent, "}"); + Separators.END_ARRAY.appendToGeneratedBuilder(sb, indent, "builder"); + } + } + else + { + append(sb, indent, "builder.Append(this." + fieldName + ");"); + } + break; + + case BEGIN_ENUM: + append(sb, indent, "builder.Append(this." + fieldName + ");"); + break; + + case BEGIN_SET: + append(sb, indent, "this." + fieldName + ".BuildString(builder);"); + break; + + case BEGIN_COMPOSITE: + { + append(sb, indent, "if (this." + fieldName + " != null)"); + append(sb, indent, "{"); + append(sb, indent, " this." + fieldName + ".BuildString(builder);"); + append(sb, indent, "}"); + append(sb, indent, "else"); + append(sb, indent, "{"); + append(sb, indent, " builder.Append(\"null\");"); + append(sb, indent, "}"); + break; + } + } + } + + final int lengthBeforeFieldSeparator = sb.length(); + Separators.FIELD.appendToGeneratedBuilder(sb, indent, "builder"); + + return lengthBeforeFieldSeparator; + } + + private void appendToString(final StringBuilder sb, final String indent) + { + sb.append('\n'); + append(sb, indent, "public override string ToString()"); + append(sb, indent, "{"); + append(sb, indent, " var sb = new StringBuilder(100);"); + append(sb, indent, " this.BuildString(sb);"); + append(sb, indent, " return sb.ToString();"); + append(sb, indent, "}"); + } + + private CharSequence generateChoiceDisplay(final String enumName) + { + final StringBuilder sb = new StringBuilder(); + + sb.append('\n'); + append(sb, INDENT, "static class " + enumName + "Ext"); + append(sb, INDENT, "{"); + append(sb, TWO_INDENT, "internal static void BuildString(this " + enumName + " val, StringBuilder builder)"); + append(sb, TWO_INDENT, "{"); + Separators.BEGIN_SET.appendToGeneratedBuilder(sb, THREE_INDENT, "builder"); + append(sb, THREE_INDENT, "builder.Append(val.ToString());"); + Separators.END_SET.appendToGeneratedBuilder(sb, THREE_INDENT, "builder"); + append(sb, TWO_INDENT, "}"); + append(sb, INDENT, "}"); + return sb; + } + + private CharSequence generateDisplay( + final String name, + final List tokens, + final List groups, + final List varData) + { + final StringBuilder sb = new StringBuilder(100); + + appendToString(sb, TWO_INDENT); + sb.append('\n'); + append(sb, TWO_INDENT, "internal void BuildString(StringBuilder builder)"); + append(sb, TWO_INDENT, "{"); + append(sb, TWO_INDENT, " if (_buffer == null)"); + append(sb, TWO_INDENT, "{"); + append(sb, TWO_INDENT, " throw new ArgumentNullException(\"_buffer\");"); + append(sb, TWO_INDENT, "}"); + sb.append('\n'); + append(sb, TWO_INDENT, " int originalLimit = this.Limit;"); + append(sb, TWO_INDENT, " this.Limit = _offset + _actingBlockLength;"); + append(sb, TWO_INDENT, " builder.Append(\"[" + name + "](sbeTemplateId=\");"); + append(sb, TWO_INDENT, " builder.Append(" + name + ".TemplateId);"); + append(sb, TWO_INDENT, " builder.Append(\"|sbeSchemaId=\");"); + append(sb, TWO_INDENT, " builder.Append(" + name + ".SchemaId);"); + append(sb, TWO_INDENT, " builder.Append(\"|sbeSchemaVersion=\");"); + append(sb, TWO_INDENT, " if (_parentMessage._actingVersion != " + name + ".SchemaVersion)"); + append(sb, TWO_INDENT, " {"); + append(sb, TWO_INDENT, " builder.Append(_parentMessage._actingVersion);"); + append(sb, TWO_INDENT, " builder.Append('/');"); + append(sb, TWO_INDENT, " }"); + append(sb, TWO_INDENT, " builder.Append(" + name + ".SchemaVersion);"); + append(sb, TWO_INDENT, " builder.Append(\"|sbeBlockLength=\");"); + append(sb, TWO_INDENT, " if (_actingBlockLength != " + name + ".BlockLength)"); + append(sb, TWO_INDENT, " {"); + append(sb, TWO_INDENT, " builder.Append(_actingBlockLength);"); + append(sb, TWO_INDENT, " builder.Append('/');"); + append(sb, TWO_INDENT, " }"); + append(sb, TWO_INDENT, " builder.Append(" + name + ".BlockLength);"); + append(sb, TWO_INDENT, " builder.Append(\"):\");"); + sb.append('\n'); + appendDisplay(sb, tokens, groups, varData, THREE_INDENT); + sb.append('\n'); + append(sb, TWO_INDENT, " this.Limit = originalLimit;"); + sb.append('\n'); + append(sb, TWO_INDENT, "}"); + return sb; + } + + private CharSequence generateCompositeDisplay(final List tokens) + { + final StringBuilder sb = new StringBuilder(); + + appendToString(sb, TWO_INDENT); + sb.append('\n'); + append(sb, TWO_INDENT, "internal void BuildString(StringBuilder builder)"); + append(sb, TWO_INDENT, "{"); + append(sb, TWO_INDENT, " if (_buffer == null)"); + append(sb, TWO_INDENT, " {"); + append(sb, TWO_INDENT, " return;"); + append(sb, TWO_INDENT, " }"); + sb.append('\n'); + Separators.BEGIN_COMPOSITE.appendToGeneratedBuilder(sb, THREE_INDENT, "builder"); + + int lengthBeforeLastGeneratedSeparator = -1; + + for (int i = 1, end = tokens.size() - 1; i < end;) + { + final Token encodingToken = tokens.get(i); + final String propertyName = formatPropertyName(encodingToken.name()); + lengthBeforeLastGeneratedSeparator = writeTokenDisplay(propertyName, encodingToken, sb, THREE_INDENT); + i += encodingToken.componentTokenCount(); + } + + if (lengthBeforeLastGeneratedSeparator > -1) + { + sb.setLength(lengthBeforeLastGeneratedSeparator); + } + + Separators.END_COMPOSITE.appendToGeneratedBuilder(sb, THREE_INDENT, "builder"); + sb.append('\n'); + append(sb, TWO_INDENT, "}"); + + return sb; + } } diff --git a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java index a43b5ce522..5985470789 100644 --- a/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java +++ b/sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/csharp/CSharpUtil.java @@ -21,13 +21,59 @@ import java.util.EnumMap; import java.util.Map; -import static uk.co.real_logic.sbe.generation.Generators.toUpperFirstChar; /** * Utilities for mapping between IR and the C# language. */ public class CSharpUtil { + + /** + * Useful separator constants for code generation + */ + public enum Separators + { + BEGIN_GROUP('['), + END_GROUP(']'), + BEGIN_COMPOSITE('('), + END_COMPOSITE(')'), + BEGIN_SET('{'), + END_SET('}'), + BEGIN_ARRAY('['), + END_ARRAY(']'), + FIELD('|'), + KEY_VALUE('='), + ENTRY(','); + + private final char symbol; + + Separators(final char symbol) + { + this.symbol = symbol; + } + + /** + * Add separator to a generated append to a {@link StringBuilder}. + * + * @param builder the code generation builder to which information should be added + * @param indent the current generated code indentation + * @param builderName the generated StringBuilder to which separator should be added + */ + public void appendToGeneratedBuilder(final StringBuilder builder, final String indent, final String builderName) + { + builder.append(indent).append(builderName).append(".Append('").append(symbol).append("');").append('\n'); + } + + /** + * Returns the string value of this separator. + * @return the string value of this separator + */ + public String toString() + { + return String.valueOf(symbol); + } + } + private static final Map PRIMITIVE_TYPE_STRING_ENUM_MAP = new EnumMap<>(PrimitiveType.class); /* @@ -59,6 +105,28 @@ public static String cSharpTypeName(final PrimitiveType primitiveType) return PRIMITIVE_TYPE_STRING_ENUM_MAP.get(primitiveType); } + /** + * Uppercase the first character of a given String. + * + * @param str to have the first character upper cased. + * @return a new String with the first character in uppercase. + */ + public static String toUpperFirstChar(final String str) + { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + /** + * Lowercase the first character of a given String. + * + * @param str to have the first character upper cased. + * @return a new String with the first character in uppercase. + */ + public static String toLowerFirstChar(final String str) + { + return Character.toLowerCase(str.charAt(0)) + str.substring(1); + } + /** * Format a String as a property name. * @@ -70,6 +138,17 @@ public static String formatPropertyName(final String str) return toUpperFirstChar(str); } + /** + * Format a String as a variable name. + * + * @param str to be formatted. + * @return the string formatted as a property name. + */ + public static String formatVariableName(final String str) + { + return toLowerFirstChar(str); + } + /** * Format a String as a class name. * @@ -80,4 +159,28 @@ public static String formatClassName(final String str) { return toUpperFirstChar(str); } + + /** + * Format a Getter name for generated code. + * + * @param propertyName to be formatted. + * @return the property name formatted as a getter name. + */ + public static String formatGetterName(final String propertyName) + { + return "Get" + toUpperFirstChar(propertyName); + } + + /** + * Shortcut to append a line of generated code + * + * @param builder string builder to which to append the line + * @param indent current text indentation + * @param line line to be appended + */ + public static void append(final StringBuilder builder, final String indent, final String line) + { + builder.append(indent).append(line).append('\n'); + } + }