All Downloads are FREE. Search and download functionalities are using the official Maven repository.

uk.co.real_logic.artio.dictionary.generation.DecoderGenerator Maven / Gradle / Ivy

There is a newer version: 0.160
Show newest version
/*
 * Copyright 2015-2023 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.AsciiNumberFormatException;
import org.agrona.LangUtil;
import org.agrona.generation.OutputManager;
import org.agrona.generation.ResourceConsumer;
import uk.co.real_logic.artio.builder.CommonDecoderImpl;
import uk.co.real_logic.artio.builder.Decoder;
import uk.co.real_logic.artio.builder.Encoder;
import uk.co.real_logic.artio.decoder.SessionHeaderDecoder;
import uk.co.real_logic.artio.dictionary.Generated;
import uk.co.real_logic.artio.dictionary.ir.Dictionary;
import uk.co.real_logic.artio.dictionary.ir.*;
import uk.co.real_logic.artio.dictionary.ir.Field.Type;
import uk.co.real_logic.artio.fields.*;
import uk.co.real_logic.artio.util.MessageTypeEncoding;

import java.io.IOException;
import java.io.Writer;
import java.util.*;
import java.util.stream.Stream;

import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static uk.co.real_logic.artio.dictionary.generation.AggregateType.*;
import static uk.co.real_logic.artio.dictionary.generation.ConstantGenerator.sizeHashSet;
import static uk.co.real_logic.artio.dictionary.generation.EncoderGenerator.encoderClassName;
import static uk.co.real_logic.artio.dictionary.generation.EnumGenerator.NULL_VAL_NAME;
import static uk.co.real_logic.artio.dictionary.generation.EnumGenerator.enumName;
import static uk.co.real_logic.artio.dictionary.generation.GenerationUtil.*;
import static uk.co.real_logic.artio.dictionary.generation.OptionalSessionFields.DECODER_OPTIONAL_SESSION_FIELDS;
import static uk.co.real_logic.artio.dictionary.generation.OptionalSessionFields.OPTIONAL_FIELD_TYPES;
import static uk.co.real_logic.sbe.generation.java.JavaUtil.formatPropertyName;

// TODO: optimisations
// skip decoding the msg type, since its known
// skip decoding the body string, since its known
// use ordering of fields to reduce branching
// skip decoding of unread header fields - eg: sender/target comp id.
// optimise the checksum definition to use an int and be calculated or ignored, have optional validation.
// evaluate utc parsing, adds about 100 nanos
// remove the REQUIRED_FIELDS validation when there are no required fields

class DecoderGenerator extends Generator
{
    private static final Set REQUIRED_SESSION_CODECS = new HashSet<>(Arrays.asList(
        "LogonDecoder",
        "LogoutDecoder",
        "RejectDecoder",
        "TestRequestDecoder",
        "SequenceResetDecoder",
        "HeartbeatDecoder",
        "ResendRequestDecoder",
        "UserRequestDecoder"));

    public static final String REQUIRED_FIELDS = "REQUIRED_FIELDS";
    private static final String GROUP_FIELDS = "GROUP_FIELDS";
    private static final String ALL_GROUP_FIELDS = "ALL_GROUP_FIELDS";

    // Has to be generated everytime since HeaderDecoder and TrailerDecoder are generated.
    private static final String MESSAGE_DECODER =
        importFor(Decoder.class) +
        importFor(Generated.class) +
        "\n" +
        GENERATED_ANNOTATION +
        "public interface MessageDecoder extends Decoder\n" +
        "{\n" +
        "    HeaderDecoder header();\n" +
        "\n" +
        "    TrailerDecoder trailer();\n" +
        "}";

    public static final int INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP =
        RejectReason.INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP.representation();
    public static final int INVALID_TAG_NUMBER =
        RejectReason.INVALID_TAG_NUMBER.representation();
    public static final int REQUIRED_TAG_MISSING =
        RejectReason.REQUIRED_TAG_MISSING.representation();
    public static final int TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE =
        RejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE.representation();
    public static final int TAG_SPECIFIED_WITHOUT_A_VALUE =
        RejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE.representation();
    public static final int VALUE_IS_INCORRECT =
        RejectReason.VALUE_IS_INCORRECT.representation();

    public static final int TAG_APPEARS_MORE_THAN_ONCE =
        RejectReason.TAG_APPEARS_MORE_THAN_ONCE.representation();
    public static final int TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER = 14;

    static String decoderClassName(final Aggregate aggregate)
    {
        return decoderClassName(aggregate.name());
    }

    static String decoderClassName(final String name)
    {
        return name + "Decoder";
    }

    private final int initialBufferSize;
    private final String encoderPackage;
    /**
     * Wrap empty buffer instead of throwing an exception if an optional string is unset.
     */
    private final boolean wrapEmptyBuffer;

    DecoderGenerator(
        final Dictionary dictionary,
        final int initialBufferSize,
        final String thisPackage,
        final String commonPackage,
        final String encoderPackage,
        final OutputManager outputManager,
        final Class validationClass,
        final Class rejectUnknownFieldClass,
        final Class rejectUnknownEnumValueClass,
        final boolean flyweightsEnabled,
        final boolean wrapEmptyBuffer,
        final String codecRejectUnknownEnumValueEnabled,
        final boolean fixTagsInJavadoc)
    {
        super(dictionary, thisPackage, commonPackage, outputManager, validationClass, rejectUnknownFieldClass,
            rejectUnknownEnumValueClass, flyweightsEnabled, codecRejectUnknownEnumValueEnabled, fixTagsInJavadoc);
        this.initialBufferSize = initialBufferSize;
        this.encoderPackage = encoderPackage;
        this.wrapEmptyBuffer = wrapEmptyBuffer;
    }

    public void generate()
    {
        generateMessageDecoderInterface();
        super.generate();
    }

    private void generateMessageDecoderInterface()
    {
        outputManager.withOutput(
            "MessageDecoder",
            (out) ->
            {
                out.append(fileHeader(thisPackage));

                out.append(MESSAGE_DECODER);
            });
    }

    protected void generateAggregateFile(final Aggregate aggregate, final AggregateType type)
    {
        if (type == COMPONENT)
        {
            componentInterface((Component)aggregate);
            return;
        }

        final String className = decoderClassName(aggregate);

        outputManager.withOutput(
            className,
            (out) ->
            {
                out.append(fileHeader(thisPackage));

                if (REQUIRED_SESSION_CODECS.contains(className))
                {
                    out.append(importFor("uk.co.real_logic.artio.decoder.Abstract" + className));
                }
                else if (type == HEADER)
                {
                    out.append(importFor(MessageTypeEncoding.class));
                    out.append(importFor(SessionHeaderDecoder.class));
                }
                out.append(importFor(AsciiNumberFormatException.class));
                out.append(importFor(Generated.class));

                generateImports(out, type);

                importEncoders(aggregate, out);

                generateAggregateClass(aggregate, type, className, out);
            });
    }

    private void importEncoders(final Aggregate aggregate, final Writer out) throws IOException
    {
        final String encoderFullClassName = encoderPackage + "." + encoderClassName(aggregate.name());
        out.write(importFor(encoderFullClassName));

        importChildEncoderClasses(aggregate, out, encoderFullClassName);
    }

    private void importChildEncoderClasses(
        final Aggregate aggregate, final Writer out, final String encoderFullClassName) throws IOException
    {
        for (final Entry entry : aggregate.entries())
        {
            if (entry.isComponent())
            {
                final String componentClassName = encoderPackage + "." + encoderClassName(entry.name());
                out.write(importFor(componentClassName));
                importChildEncoderClasses((Aggregate)entry.element(), out, componentClassName);
            }
            else if (entry.isGroup())
            {
                final String entryClassName = encoderClassName(entry.name());
                out.write(importStaticFor(encoderFullClassName, entryClassName));
                importChildEncoderClasses(
                    (Aggregate)entry.element(), out, encoderFullClassName + "." + entryClassName);
            }
        }
    }

    private void generateAggregateClass(
        final Aggregate aggregate,
        final AggregateType type,
        final String className,
        final Writer out) throws IOException
    {
        push(aggregate);

        final boolean isMessage = type == MESSAGE;
        final boolean isGroup = type == GROUP;
        final List interfaces = aggregate
            .componentEntries()
            .map((comp) -> decoderClassName((Aggregate)comp.element()))
            .collect(toList());

        if (isMessage)
        {
            interfaces.add("MessageDecoder");

            if (REQUIRED_SESSION_CODECS.contains(className))
            {
                interfaces.add("Abstract" + className);
            }
        }
        else if (type == HEADER)
        {
            interfaces.add(SessionHeaderDecoder.class.getSimpleName());
        }

        out.append(classDeclaration(className, interfaces, false, aggregate.isInParent(), isGroup));
        generateValidation(out, aggregate, type);
        if (isMessage)
        {
            final Message message = (Message)aggregate;
            // Normally generate these, but if its shared only generate it in the parent
            if (!message.isInParent() || isSharedParent())
            {
                out.append(messageType(message.fullType(), message.packedType()));
            }

            if (!isSharedParent())
            {
                final List fields = compileAllFieldsFor(message);
                final String messageFieldsSet = generateFieldDictionary(fields, MESSAGE_FIELDS, false);
                out.append(commonCompoundImports("Decoder", true, messageFieldsSet));
            }
        }
        groupMethods(out, aggregate);
        headerMethods(out, aggregate, type);
        generateGetters(out, className, aggregate.entries(), aggregate.isInParent());
        out.append(decodeMethod(aggregate.entries(), aggregate, type));
        out.append(completeResetMethod(
            isMessage, aggregate.entries(), additionalReset(isGroup), aggregate.isInParent()));
        out.append(generateAppendTo(aggregate, isMessage));
        out.append(generateToEncoder(aggregate));
        out.append("}\n");
        pop();
    }

    private String classDeclaration(
        final String className,
        final List interfaces,
        final boolean isStatic,
        final boolean inParent,
        final boolean isGroup)
    {
        final String interfaceList = interfaces.isEmpty() ? "" : " implements " + String.join(", ", interfaces);

        final String extendsClause;
        if (inParent)
        {
            String qualifiedName = className;
            // Groups are inner classes in their parent
            if (isGroup)
            {
                qualifiedName = qualifiedSharedAggregateDecoderNames();
            }
            extendsClause = parentDictPackage() + "." + qualifiedName;
        }
        else
        {
            extendsClause = "CommonDecoderImpl";
        }
        return String.format(
            "\n" +
            GENERATED_ANNOTATION +
            "public %3$s%4$sclass %1$s extends %5$s%2$s\n" +
            "{\n",
            className,
            interfaceList,
            isStatic ? "static " : "",
            isSharedParent() ? "abstract " : "",
            extendsClause);
    }

    private String qualifiedSharedAggregateDecoderNames()
    {
        return qualifiedAggregateStackNames(aggregate ->
            (aggregate instanceof Group ? "Abstract" : "") + decoderClassName(aggregate.name()));
    }

    private List compileAllFieldsFor(final Message message)
    {
        final Stream messageBodyFields = extractFields(message.entries());
        final Stream headerFields = extractFields(dictionary.header().entries());
        final Stream trailerFields = extractFields(dictionary.trailer().entries());

        return Stream.of(headerFields, messageBodyFields, trailerFields)
            .reduce(Stream::concat)
            .orElseGet(Stream::empty)
            .collect(toList());
    }

    private void headerMethods(final Writer out, final Aggregate aggregate, final AggregateType type)
        throws IOException
    {
        if (type == HEADER && !isSharedParent())
        {
            // Default constructor so that the header decoder can be used independently to parser headers.
            out.append(
                "    public HeaderDecoder()\n" +
                "    {\n" +
                "        this(new TrailerDecoder());\n" +
                "    }\n\n");
            wrapTrailerInConstructor(out, aggregate);

            generatePackedMessageTypeMethod(out);
        }
    }

    private void generatePackedMessageTypeMethod(final Writer out) throws IOException
    {
        if (flyweightsEnabled)
        {
            out.append(
                "    public long messageType()\n" +
                "    {\n" +
                "        return buffer.getMessageType(msgTypeOffset, msgTypeLength);\n" +
                "    }\n\n");
        }
        else
        {
            out.append(
                "    public long messageType()\n" +
                "    {\n" +
                "        return MessageTypeEncoding.packMessageType(msgType(), msgTypeLength());\n" +
                "    }\n\n");
        }
    }

    private void groupClass(final Group group, final Writer out) throws IOException
    {
        final String className = groupClassName(group);
        generateAggregateClass(group, GROUP, className, out);
    }

    private String groupClassName(final Group group)
    {
        String className = decoderClassName(group);
        if (isSharedParent())
        {
            // group classes on components can be imported from two different components so we prefix Abstract to the
            // name of the abstract parent group class in order to resolve the ambiguity.
            className = "Abstract" + className;
        }
        return className;
    }

    protected Class topType(final AggregateType aggregateType)
    {
        return Decoder.class;
    }

    protected String resetGroup(final Entry entry)
    {
        final Group group = (Group)entry.element();
        final String name = group.name();
        final String resetMethod = nameOfResetMethod(name);

        if (isSharedParent())
        {
            return String.format("    public abstract void %1$s();\n", resetMethod);
        }
        else
        {
            final Entry numberField = group.numberField();
            return String.format(
                "    public void %1$s()\n" +
                "    {\n" +
                "        for (final %2$s %6$s : %5$s.iterator())\n" +
                "        {\n" +
                "            %6$s.reset();\n" +
                "            if (%6$s.next() == null)\n" +
                "            {\n" +
                "                break;\n" +
                "            }\n" +
                "        }\n" +
                "        %3$s = MISSING_INT;\n" +
                "        has%4$s = false;\n" +
                "    }\n\n",
                resetMethod,
                decoderClassName(name),
                formatPropertyName(numberField.name()),
                numberField.name(),
                iteratorFieldName(group),
                formatPropertyName(decoderClassName(name)));
        }
    }

    private String iteratorClassName(final Group group, final boolean ofParent)
    {
        final String prefix = ofParent ? "Abstract" : "";
        return prefix + group.name() + "Iterator";
    }

    private String iteratorFieldName(final Group group)
    {
        return formatPropertyName(iteratorClassName(group, false));
    }

    protected String resetLength(final String name)
    {
        return String.format(
            "    public void %1$s()\n" +
            "    {\n" +
            "        %2$sLength = 0;\n" +
            "    }\n\n",
            nameOfResetMethod(name),
            formatPropertyName(name));
    }

    protected String resetRequiredFloat(final String name)
    {
        final String lengthReset = flyweightsEnabled ? "        %1$sLength = 0;\n" : "";

        return String.format(
            "    public void %2$s()\n" +
            "    {\n" +
            lengthReset +
            "        %1$s.reset();\n" +
            "    }\n\n",
            formatPropertyName(name),
            nameOfResetMethod(name));
    }

    protected String resetRequiredInt(final Field field)
    {
        return resetFieldValue(field, "MISSING_INT");
    }

    protected String resetRequiredLong(final Field field)
    {
        return resetFieldValue(field, "MISSING_LONG");
    }

    private String additionalReset(final boolean isGroup)
    {
        return
            "        buffer = null;\n" +
            "        if (" + CODEC_VALIDATION_ENABLED + ")\n" +
            "        {\n" +
            "            invalidTagId = Decoder.NO_ERROR;\n" +
            "            rejectReason = Decoder.NO_ERROR;\n" +
            "            missingRequiredFields.clear();\n" +
            (isGroup ? "" :
                "            unknownFields.clear();\n" +
                "            alreadyVisitedFields.clear();\n") +
            "        }\n";
    }

    private void generateValidation(final Writer out, final Aggregate aggregate, final AggregateType type)
        throws IOException
    {
        if (isSharedParent())
        {
            out.append("    public final IntHashSet " + REQUIRED_FIELDS + " = new IntHashSet();\n\n");
            return;
        }

        final List requiredFields = requiredFields(aggregate.entries()).collect(toList());
        out.append(generateFieldDictionary(requiredFields, REQUIRED_FIELDS, true));

        if (aggregate.containsGroup())
        {
            final List groupFields = aggregate
                .allFieldsIncludingComponents()
                .map(Entry::element)
                .map(element -> (Field)element)
                .collect(toList());
            final String groupFieldString = generateFieldDictionary(groupFields, GROUP_FIELDS, true);
            out.append(groupFieldString);
        }

        final String enumValidation = aggregate
            .allFieldsIncludingComponents()
            .filter((entry) -> entry.element().isEnumField())
            .map(this::generateEnumValidation)
            .collect(joining("\n"));

        //maybe this should look at groups on components too?
        final String groupValidation = aggregate
            .allGroupsIncludingComponents()
            .map(this::generateGroupValidation)
            .collect(joining("\n"));

        final boolean isMessage = type == MESSAGE;
        final boolean isGroup = type == GROUP;
        final String messageValidation = isMessage ?
            "        if (" + CODEC_REJECT_UNKNOWN_FIELD_ENABLED + " && unknownFieldsIterator.hasNext())\n" +
            "        {\n" +
            "            invalidTagId = unknownFieldsIterator.nextValue();\n" +
            "            rejectReason = Constants.ALL_FIELDS.contains(invalidTagId) ? " +
            TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE + " : " + INVALID_TAG_NUMBER + ";\n" +
            "            return false;\n" +
            "        }\n" +
            "        if (!header.validate())\n" +
            "        {\n" +
            "            invalidTagId = header.invalidTagId();\n" +
            "            rejectReason = header.rejectReason();\n" +
            "            return false;\n" +
            "        }\n" +
            "        else if (!trailer.validate())\n" +
            "        {\n" +
            "            invalidTagId = trailer.invalidTagId();\n" +
            "            rejectReason = trailer.rejectReason();\n" +
            "            return false;\n" +
            "        }\n" :
            "";

        out.append(String.format(
            (isGroup ? generateAllGroupFields(aggregate) :
            "    private final IntHashSet alreadyVisitedFields = new IntHashSet(%5$d);\n\n" +
            "    private final IntHashSet unknownFields = new IntHashSet(10);\n\n") +
            "    private final IntHashSet missingRequiredFields = new IntHashSet(%1$d);\n\n" +
            "    public boolean validate()\n" +
            "    {\n" +
            // validation for some tags performed in the decode method
            "        if (rejectReason != Decoder.NO_ERROR)\n" +
            "        {\n" +
            "            return false;\n" +
            "        }\n" +
            "        final IntIterator missingFieldsIterator = missingRequiredFields.iterator();\n" +
            (isMessage ? "        final IntIterator unknownFieldsIterator = unknownFields.iterator();\n" : "") +
            "%2$s" +
            "        if (missingFieldsIterator.hasNext())\n" +
            "        {\n" +
            "            invalidTagId = missingFieldsIterator.nextValue();\n" +
            "            rejectReason = " + REQUIRED_TAG_MISSING + ";\n" +
            "            return false;\n" +
            "        }\n" +
            "%3$s" +
            "%4$s" +
            "        return true;\n" +
            "    }\n\n",
            sizeHashSet(requiredFields),
            messageValidation,
            enumValidation,
            groupValidation,
            2 * aggregate.allFieldsIncludingComponents().count()));
    }

    private String generateAllGroupFields(final Aggregate groupAggregate)
    {
        final List allGroupFields = groupAggregate
            .fieldEntries()
            .map((entry) -> (Field)entry.element())
            .collect(toList());
        return generateFieldDictionary(allGroupFields, ALL_GROUP_FIELDS, true);
    }

    private String generateFieldDictionary(
        final Collection fields, final String name, final boolean shouldGenerateValidationGating)
    {
        final String addFields = fields
            .stream()
            .map((field) -> addField(field, name, shouldGenerateValidationGating ? "            " : "        "))
            .collect(joining());
        final String generatedFieldEntryCode;

        if (shouldGenerateValidationGating)
        {
            generatedFieldEntryCode =
                "        if (" + CODEC_VALIDATION_ENABLED + ")\n" +
                "        {\n" +
                "%s" +
                "        }\n";
        }
        else
        {
            generatedFieldEntryCode = "%s";
        }

        final int hashMapSize = sizeHashSet(fields);
        return String.format(
            "    public final IntHashSet %2$s = new IntHashSet(%1$d);\n\n" +
            "    {\n" +
            "%3$s" +
            "    }\n\n",
            hashMapSize,
            name,
            String.format(generatedFieldEntryCode, addFields));
    }

    public static String addField(final Field field, final String name, final String prefix)
    {
        return String.format(
            "%1$s%2$s.add(Constants.%3$s);\n",
            prefix,
            name,
            constantName(field.name()));
    }

    private CharSequence generateEnumValidation(final Entry entry)
    {
        final Field field = (Field)entry.element();
        if (!EnumGenerator.hasEnumGenerated(field))
        {
            return "";
        }

        final String name = entry.name();
        final int tagNumber = field.number();
        final Type type = field.type();
        final String propertyName = formatPropertyName(name);

        final boolean isPrimitive = type.isIntBased() || type == Type.CHAR;

        final String enumValidation = String.format(
            "        if (" + codecRejectUnknownEnumValueEnabled + " && !%1$s.isValid(%2$s%4$s))\n" +
            "        {\n" +
            "            invalidTagId = %3$s;\n" +
            "            rejectReason = " + VALUE_IS_INCORRECT + ";\n" +
            "            return false;\n" +
            "        }\n",
            enumName(name),
            propertyName,
            tagNumber,
            isPrimitive ? "()" : "Wrapper");

        final String enumValidationMethod;
        if (type.isMultiValue())
        {
            enumValidationMethod =
                String.format(
                    "          int %1$sOffset = 0;\n" +
                    "          for (int i = 0; i < %1$sLength; i++)\n" +
                    "          {\n" +
                    "              if (this.%1$s()[i] == ' ')\n" +
                    "              {\n" +
                    "                  %1$sWrapper.wrap(this.%1$s(), %1$sOffset, i - %1$sOffset);\n" +
                    "%2$s" +
                    "                  %1$sOffset = i + 1;\n" +
                    "              }\n" +
                    "          }\n" +
                    "          %1$sWrapper.wrap(this.%1$s(), %1$sOffset, %1$sLength - %1$sOffset);\n" +
                    "%2$s",
                    propertyName,
                    enumValidation
                );
        }
        else
        {
            enumValidationMethod =
                String.format(
                    (isPrimitive ? "" : "        %1$sWrapper.wrap(this.%1$s(), %1$sLength);\n") +
                    "%2$s",
                    propertyName,
                    enumValidation
                );
        }

        return
            entry.required() ? enumValidationMethod :
            String.format(
                "        if (has%1$s)\n" +
                "        {\n" +
                "%2$s" +
                "        }\n",
                entry.name(),
                enumValidationMethod
            );
    }

    private CharSequence generateGroupValidation(final Entry entry)
    {
        final Group group = (Group)entry.element();
        final Entry numberField = group.numberField();
        final String numberFieldName = numberField.name();
        final boolean required = entry.required();
        final String validationCode = String.format(
            "%3$s        {\n" +
            "%3$s            int count = 0;\n" +
            "%3$s            final %4$s iterator = %2$s.iterator();\n" +
            "%3$s            for (final %1$s group : iterator)\n" +
            "%3$s            {\n" +
            "%3$s                count++;" +
            "%3$s                if (!group.validate())\n" +
            "%3$s                {\n" +
            "%3$s                    invalidTagId = group.invalidTagId();\n" +
            "%3$s                    rejectReason = group.rejectReason();\n" +
            "%3$s                    return false;\n" +
            "%3$s                }\n" +
            "%3$s            }\n" +
            "%3$s            if (count != iterator.numberFieldValue())\n" +
            "%3$s            {\n" +
            "%3$s                invalidTagId = %5$s;\n" +
            "%3$s                rejectReason = %6$s;\n" +
            "%3$s                return false;\n" +
            "%3$s            }\n" +
            "%3$s        }\n",
            decoderClassName(group),
            iteratorFieldName(group),
            required ? "" : "    ",
            iteratorClassName(group, false),
            numberField.number(),
            INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP);

        if (required)
        {
            return validationCode;
        }
        else
        {
            return String.format(
                "        if (has%1$s)\n" +
                "        {\n" +
                "%2$s" +
                "        }\n",
                numberFieldName,
                validationCode
            );
        }
    }

    private Stream requiredFields(final List entries)
    {
        return entries
            .stream()
            .filter(Entry::required)
            .flatMap(this::extractRequiredFields);
    }

    private Stream extractRequiredFields(final Entry entry)
    {
        return entry.match(
            (e, field) -> Stream.of(field),
            (e, group) -> Stream.of((Field)group.numberField().element()),
            (e, component) -> requiredFields(component.entries()));
    }

    private Stream extractFields(final Entry entry)
    {
        return entry.match(
            (e, field) -> Stream.of(field),
            (e, group) -> Stream.concat(Stream.of((Field)group.numberField().element()),
                                group.entries().stream().flatMap(this::extractFields)),
            (e, component) -> component.entries().stream().flatMap(this::extractFields));
    }

    private Stream extractFields(final List entries)
    {

        return entries.stream().flatMap(this::extractFields);
    }

    private void componentInterface(final Component component)
    {
        push(component);

        final String className = decoderClassName(component);
        outputManager.withOutput(className,
            (out) ->
            {
                out.append(fileHeader(thisPackage));
                final List interfaces = component.allComponents()
                    .map((comp) -> decoderClassName((Aggregate)comp.element()))
                    .collect(toList());

                if (component.isInParent())
                {
                    interfaces.add(parentDictPackage() + "." + className);
                }

                final String interfaceExtension = interfaces.isEmpty() ? "" : " extends " +
                    String.join(", ", interfaces);

                out.append(importFor(AsciiNumberFormatException.class));
                generateImports(out, AggregateType.COMPONENT);
                importEncoders(component, out);
                out.append(importFor(Generated.class));

                out.append(String.format(
                    "\n" +
                    GENERATED_ANNOTATION +
                    "public interface %1$s %2$s\n" +
                    "{\n\n",
                    className,
                    interfaceExtension));

                for (final Entry entry : component.entries())
                {
                    // We need to generate the group entry in the component interface even if the entry is in the
                    // parent interface because we've got a more specialised Group + Iterator class to generate
                    if (!entry.isInParent() || entry.isGroup())
                    {
                        componentInterfaceGetter(entry, out);
                    }
                }
                out.append("\n}\n");
            });

        pop();
    }

    private void generateImports(final Writer out, final AggregateType component) throws IOException
    {
        generateImports("Decoder", component, out,
            Encoder.class, CommonDecoderImpl.class);
    }

    private void componentInterfaceGetter(final Entry entry, final Writer out)
    {
        entry.forEach(
            (field) -> out.append(fieldInterfaceGetter(entry, field)),
            (group) -> groupInterfaceGetter(group, out),
            (component) -> {});
    }

    private void groupInterfaceGetter(final Group group, final Writer out) throws IOException
    {
        groupClass(group, out);
        generateGroupIterator(out, group);

        final Entry numberField = group.numberField();
        final boolean ofParent = isSharedParent();
        out.append(String.format("    public %1$s %2$s();\n",
            iteratorClassName(group, ofParent),
            iteratorFieldName(group)));
        out.append(fieldInterfaceGetter(numberField, (Field)numberField.element()));

        out.append(String.format(
            "    public %1$s %2$s();\n",
            groupClassName(group),
            formatPropertyName(group.name())));
    }

    private void wrappedForEachEntry(
        final Aggregate aggregate, final Writer out, final ResourceConsumer consumer)
        throws IOException
    {
        out.append("\n");
        aggregate
            .entries()
            .forEach((t) ->
            {
                try
                {
                    consumer.accept(t);
                }
                catch (final IOException ex)
                {
                    LangUtil.rethrowUnchecked(ex);
                }
            });
        out.append("\n");
    }

    private String fieldInterfaceGetter(final Entry entry, final Field field)
    {
        final String name = field.name();
        final String fieldName = formatPropertyName(name);
        final Type type = field.type();

        final String javadoc = generateAccessorJavadoc(field);

        final String length = type.isStringBased() ?
            String.format("    %2$spublic int %1$sLength();\n", fieldName, javadoc) : "";

        final String stringAsciiView = type.isStringBased() ?
            String.format("    %2$spublic AsciiSequenceView %1$s(AsciiSequenceView view);\n", fieldName, javadoc) : "";

        final String optional = !entry.required() ?
            String.format("    %2$spublic boolean has%1$s();\n", name, javadoc) : "";

        final String enumDecoder = shouldGenerateClassEnumMethods(field) ?
            String.format("    %3$spublic %1$s %2$sAsEnum();\n", name, fieldName, javadoc) : "";

        return String.format(
            "    %7$spublic %1$s %2$s();\n" +
            "%3$s" +
            "%4$s" +
            "%5$s" +
            "%6$s",
            javaTypeOf(type),
            fieldName,
            optional,
            length,
            enumDecoder,
            stringAsciiView,
            javadoc);
    }

    private void generateGetter(
        final Entry entry, final Writer out, final Set missingOptionalFields,
        final boolean aggregateIsInParent)
    {
        entry.forEach(
            (field) ->
            {
                missingOptionalFields.remove(field.name());
                out.append(fieldGetter(entry, field, aggregateIsInParent));
            },
            (group) -> groupGetter(group, out, aggregateIsInParent),
            (component) ->
            {
                // Components can be shared, but without every message of the given type implementing that component
                final boolean componentIsInParent = aggregateIsInParent && entry.isInParent();
                componentGetter(component, out, missingOptionalFields, componentIsInParent);
            });
    }

    private void groupMethods(final Writer out, final Aggregate aggregate) throws IOException
    {
        if (aggregate instanceof Group)
        {
            final Group group = (Group)aggregate;

            wrapTrailerAndMessageFieldsInGroupConstructor(out, group);

            out.append(String.format(
                "    private %1$s next = null;\n\n" +
                "    public %1$s next()\n" +
                "    {\n" +
                "        return next;\n" +
                "    }\n\n" +
                "    private IntHashSet seenFields = new IntHashSet(%2$d);\n\n",
                groupClassName(group),
                sizeHashSet(group.entries())));
        }
    }

    private void wrapTrailerAndMessageFieldsInGroupConstructor(final Writer out, final Aggregate aggregate)
        throws IOException
    {
        if (isSharedParent())
        {
            return;
        }

        out.append(String.format(
            "    private final TrailerDecoder trailer;\n" +
            "    private final IntHashSet %1$s;\n" +
            "    public %2$s(final TrailerDecoder trailer, final IntHashSet %1$s)\n" +
            "    {\n" +
            "        this.trailer = trailer;\n" +
            "        this.%1$s = %1$s;\n" +
            "    }\n\n",
            MESSAGE_FIELDS,
            decoderClassName(aggregate)));
    }

    private void wrapTrailerInConstructor(final Writer out, final Aggregate aggregate) throws IOException
    {
        out.append(String.format(
            "    private final TrailerDecoder trailer;\n" +
            "    public %1$s(final TrailerDecoder trailer)\n" +
            "    {\n" +
            "        this.trailer = trailer;\n" +
            "    }\n\n",
            decoderClassName(aggregate)));
    }

    private String messageType(final String fullType, final long packedType)
    {
        return String.format(
            "    public static final long MESSAGE_TYPE = %1$dL;\n\n" +
            "    public static final String MESSAGE_TYPE_AS_STRING = \"%2$s\";\n\n" +
            "    public static final char[] MESSAGE_TYPE_CHARS = MESSAGE_TYPE_AS_STRING.toCharArray();\n\n" +
            "    public static final byte[] MESSAGE_TYPE_BYTES = MESSAGE_TYPE_AS_STRING.getBytes(US_ASCII);\n\n",
            packedType,
            fullType);
    }

    private void generateGetters(
        final Writer out, final String className, final List entries, final boolean aggregateIsInParent)
        throws IOException
    {
        final List optionalFields = DECODER_OPTIONAL_SESSION_FIELDS.get(className);
        // Remove from missingOptionalFields during generateGetter
        final Set missingOptionalFields = optionalFields == null ? emptySet() : new HashSet<>(optionalFields);

        for (final Entry entry : entries)
        {
            generateGetter(entry, out, missingOptionalFields, aggregateIsInParent);
        }

        generateMissingOptionalSessionFields(out, missingOptionalFields);
        generateOptionalSessionFieldsSupportedMethods(optionalFields, missingOptionalFields, out);
    }

    private void generateMissingOptionalSessionFields(
        final Writer out, final Set missingOptionalFields)
        throws IOException
    {
        for (final String optionalField : missingOptionalFields)
        {
            final String propertyName = formatPropertyName(optionalField);
            final Type type = OPTIONAL_FIELD_TYPES.get(optionalField);
            switch (type)
            {
                case STRING:
                {
                    out.append(String.format(
                        "    public char[] %1$s()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n" +
                        "    public boolean has%2$s()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n" +
                        "    public int %1$sLength()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n" +
                        "    public String %1$sAsString()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n" +
                        "    public AsciiSequenceView %1$s(final AsciiSequenceView view)\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n",
                        propertyName,
                        optionalField));
                    break;
                }

                case INT:
                {
                    out.append(String.format(
                        "    public int %1$s()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n" +
                        "    public boolean has%2$s()\n" +
                        "    {\n" +
                        "        throw new UnsupportedOperationException();\n" +
                        "    }\n\n",
                        propertyName,
                        optionalField));
                    break;
                }

                default:
                    throw new UnsupportedOperationException("Unknown field type for: '" + optionalField + "'");
            }
        }
    }

    private void componentGetter(
        final Component component,
        final Writer out,
        final Set missingOptionalFields,
        final boolean aggregateIsInParent)
        throws IOException
    {
        push(component);
        wrappedForEachEntry(component, out,
            (entry) -> generateGetter(entry, out, missingOptionalFields, aggregateIsInParent));
        pop();
    }

    private void groupGetter(
        final Group group,
        final Writer out,
        final boolean aggregateIsShared)
        throws IOException
    {
        // The component interface will generate the group class
        final Aggregate currentAggregate = currentAggregate();
        final boolean inComponent = currentAggregate instanceof Component;
        if (!inComponent)
        {
            groupClass(group, out);
            generateGroupIterator(out, group);
        }

        final Entry numberField = group.numberField();
        // Try to generate the number field as high up the hierarchy as possible.
        // In the parent class if it's there, if it's a component then that's an interface so it can't be generated
        // there
        final String prefix = group.isInParent() && (aggregateIsShared && !inComponent) ? "" : fieldGetter(
            numberField, (Field)numberField.element(), aggregateIsShared);

        final String groupClassName = groupClassName(group);

        if (isSharedParent())
        {
            out.append(String.format(
                "\n" +
                "    public abstract %1$s %2$s();\n\n" +
                "%3$s\n" +
                "    public abstract %4$s %5$s();\n\n",
                groupClassName,
                formatPropertyName(group.name()),
                prefix,
                iteratorClassName(group, true),
                iteratorFieldName(group)));
        }
        else
        {
            out.append(String.format(
                "\n" +
                "    private %1$s %2$s = null;\n" +
                "    public %1$s %2$s()\n" +
                "    {\n" +
                "        return %2$s;\n" +
                "    }\n\n" +
                "%3$s\n" +
                "    private %4$s %5$s = new %4$s(this);\n" +
                "    public %4$s %5$s()\n" +
                "    {\n" +
                "        return %5$s.iterator();\n" +
                "    }\n\n",
                groupClassName,
                formatPropertyName(group.name()),
                prefix,
                iteratorClassName(group, false),
                iteratorFieldName(group)));
        }
    }

    private void generateGroupIterator(final Writer out, final Group group) throws IOException
    {
        final String numberFieldName = group.numberField().name();
        final String formattedNumberFieldName = formatPropertyName(numberFieldName);
        final String numberFieldReset =
            group.numberField().required() ?
            String.format("parent.%1$s()", formattedNumberFieldName) :
            String.format("parent.has%1$s() ? parent.%2$s() : 0", numberFieldName, formattedNumberFieldName);

        final boolean sharedParent = isSharedParent();
        final String iteratorClassName = iteratorClassName(group, sharedParent);
        final String groupDecoderName = groupClassName(group);
        final String parentDecoderName = qualifiedSharedAggregateDecoderNames();
        final String groupPropertyName = formatPropertyName(group.name());

        if (sharedParent)
        {
            out.append(String.format(
                "    " + GENERATED_ANNOTATION +
                "    public abstract class %1$s implements Iterable, java.util.Iterator\n" +
                "    {\n" +
                "        private final %3$s parent;\n" +
                "        private int remainder;\n" +
                "        private T current;\n\n" +
                "        public %1$s(final %3$s parent)\n" +
                "        {\n" +
                "            this.parent = parent;\n" +
                "        }\n\n" +
                "        public boolean hasNext()\n" +
                "        {\n" +
                "            return remainder > 0 && current != null;\n" +
                "        }\n\n" +
                "        @SuppressWarnings(\"unchecked\")\n" +
                "        public T next()\n" +
                "        {\n" +
                "            remainder--;\n" +
                "            final T value = current;\n" +
                "            current = (T)current.next();\n" +
                "            return value;\n" +
                "        }\n\n" +
                "        public int numberFieldValue()\n" +
                "        {\n" +
                "            return %4$s;\n" +
                "        }\n\n" +
                "        @SuppressWarnings(\"unchecked\")\n" +
                "        public void reset()\n" +
                "        {\n" +
                "            remainder = numberFieldValue();\n" +
                "            current = (T)parent.%5$s();\n" +
                "        }\n\n" +
                "    }\n\n",
                iteratorClassName,
                groupDecoderName,
                parentDecoderName,
                numberFieldReset,
                groupPropertyName));
        }
        else if (group.isInParent())
        {
            final String parentLocation = parentDictPackage() + "." + parentDecoderName + "." +
                iteratorClassName(group, true);
            out.append(String.format(
                "    public class %1$s extends %4$s<%2$s>\n" +
                "    {\n" +
                "        public %1$s(final %3$s parent)\n" +
                "        {\n" +
                "            super(parent);\n" +
                "        }\n\n" +
                "        public %1$s iterator()\n" +
                "        {\n" +
                "            reset();\n" +
                "            return this;\n" +
                "        }\n\n" +
                "    }\n\n",
                iteratorClassName,
                groupDecoderName,
                parentDecoderName,
                parentLocation));
        }
        else
        {
            generateNormalGroupIterator(
                out, numberFieldReset, iteratorClassName, groupDecoderName, groupPropertyName);
        }
    }

    private void generateNormalGroupIterator(
        final Writer out,
        final String numberFieldReset,
        final String iteratorClassName,
        final String groupDecoderName,
        final String groupPropertyName)
        throws IOException
    {
        final String parentDecoderName = decoderClassName(currentAggregate());
        out.append(String.format(
            "    " + GENERATED_ANNOTATION +
            "    public class %1$s implements Iterable<%2$s>, java.util.Iterator<%2$s>\n" +
            "    {\n" +
            "        private final %3$s parent;\n" +
            "        private int remainder;\n" +
            "        private %2$s current;\n\n" +
            "        public %1$s(final %3$s parent)\n" +
            "        {\n" +
            "            this.parent = parent;\n" +
            "        }\n\n" +
            "        public boolean hasNext()\n" +
            "        {\n" +
            "            return remainder > 0 && current != null;\n" +
            "        }\n\n" +
            "        public %2$s next()\n" +
            "        {\n" +
            "            remainder--;\n" +
            "            final %2$s value = current;\n" +
            "            current = current.next();\n" +
            "            return value;\n" +
            "        }\n\n" +
            "        public int numberFieldValue()\n" +
            "        {\n" +
            "            return %4$s;\n" +
            "        }\n\n" +
            "        public void reset()\n" +
            "        {\n" +
            "            remainder = numberFieldValue();\n" +
            "            current = parent.%5$s();\n" +
            "        }\n\n" +
            "        public %1$s iterator()\n" +
            "        {\n" +
            "            reset();\n" +
            "            return this;\n" +
            "        }\n\n" +
            "    }\n\n",
            iteratorClassName,
            groupDecoderName,
            parentDecoderName,
            numberFieldReset,
            groupPropertyName));
    }

    private String fieldGetter(
        final Entry entry,
        final Field field,
        final boolean aggregateIsInParent)
    {
        // Entry may exist in common parent component interface but there may not be a common parent message
        if (entry.isInParent() && aggregateIsInParent)
        {
            return "";
        }

        final String name = field.name();
        final String fieldName = formatPropertyName(name);
        final Type type = field.type();
        final String optionalCheck = optionalCheck(entry);
        final String asStringBody = generateAsStringBody(entry, name, fieldName);
        final String javadoc = generateAccessorJavadoc(field);

        final String extraStringDecode = type.isStringBased() ? String.format(
            "    %4$spublic String %1$sAsString()\n" +
            "    {\n" +
            "        return %3$s;\n" +
            "    }\n\n" +
            "    %4$spublic AsciiSequenceView %1$s(final AsciiSequenceView view)\n" +
            "    {\n" +
            "%2$s" +
            "        return view.wrap(buffer, %1$sOffset, %1$sLength);\n" +
            "    }\n\n",
            fieldName, wrapEmptyBuffer ? wrapEmptyBuffer(entry) : optionalCheck, asStringBody, javadoc) : "";

        // Need to keep offset and length split due to the abject fail that is the DATA type.
        final String lengthBasedFields = type.hasLengthField(flyweightsEnabled) ? String.format(
            "    %4$s int %1$sLength;\n\n" +
            "    %5$spublic int %1$sLength()\n" +
            "    {\n" +
            "%2$s" +
            "        return %1$sLength;\n" +
            "    }\n\n" +
            "%3$s",
            fieldName, optionalCheck, extraStringDecode, scope, javadoc) : "";

        final String offsetField = type.hasOffsetField(flyweightsEnabled) ?
            String.format("    %3$s int %1$sOffset;\n\n%2$s", fieldName, lengthBasedFields, scope) : "";

        final String enumValueDecoder = String.format(
            type.isStringBased() ?
            "%1$s.decode(%2$sWrapper)" :
            // Need to ensure that decode the field
            (flyweightsEnabled && (type.isIntBased() || type.isFloatBased())) ?
            "%1$s.decode(this.%2$s())" :
            "%1$s.decode(%2$s)",
            enumName(name),
            fieldName);
        final String enumStringBasedWrapperField =
            String.format("    %2$s final CharArrayWrapper %1$sWrapper = new CharArrayWrapper();\n", fieldName, scope);
        final String enumDecoder = shouldGenerateClassEnumMethods(field) ?
            String.format(
            "%4$s" +
            "    %7$spublic %6$s %2$sAsEnum()\n" +
            "    {\n" +
            (!entry.required() ? "        if (!has%1$s)\n return %6$s.%5$s;\n" : "") +
            (type.isStringBased() ? "        %2$sWrapper.wrap(this.%2$s(), %2$sLength);\n" : "") +
            "        return %3$s;\n" +
            "    }\n\n",
            name, fieldName, enumValueDecoder, enumStringBasedWrapperField, NULL_VAL_NAME, enumName(name),
            javadoc) : (field.type().isMultiValue() || field.type() == Type.STRING) ? enumStringBasedWrapperField : "";

        final String lazyInitialisation = fieldLazyInstantialisation(field, fieldName);

        return String.format(
            "    %10$s %1$s %2$s%3$s;\n\n" +
            "%4$s" +
            "    %11$spublic %1$s %2$s()\n" +
            "    {\n" +
            "%5$s" +
            "%9$s" +
            "        return %2$s;\n" +
            "    }\n\n" +
            "%6$s\n" +
            "%7$s\n" +
            "%8$s",
            javaTypeOf(type),
            fieldName,
            fieldInitialisation(type),
            hasField(entry),
            optionalCheck,
            optionalGetter(entry),
            offsetField,
            enumDecoder,
            flyweightsEnabled ? lazyInitialisation : "",
            scope,
            javadoc);
    }

    private String wrapEmptyBuffer(final Entry entry)
    {
        return entry.required() ? "" : String.format(
          "        if (!has%s)\n" +
          "        {\n" +
          "            return view.wrap(buffer, 0, 0);\n" +
          "        }\n\n",
          entry.name());
    }

    private String generateAsStringBody(final Entry entry, final String name, final String fieldName)
    {
        final String asStringBody;

        if (flyweightsEnabled)
        {
            asStringBody = String.format(entry.required() ?
                "buffer != null ? buffer.getStringWithoutLengthAscii(%1$sOffset, %1$sLength) : \"\"" :
                "has%2$s ? buffer.getStringWithoutLengthAscii(%1$sOffset, %1$sLength) : null",
                fieldName,
                name);
        }
        else
        {
            asStringBody = String.format(entry.required() ?
                "new String(%1$s, 0, %1$sLength)" :
                "has%2$s ? new String(%1$s, 0, %1$sLength) : null",
                fieldName,
                name);
        }
        return asStringBody;
    }

    private static String fieldLazyInstantialisation(final Field field, final String fieldName)
    {
        final int tag = field.number();
        switch (field.type())
        {
            // We read and cache the number in group field so that it doesn't get re-read during a reset.
            case NUMINGROUP:
                return String.format(
                    "%1$s = groupNoField(buffer, %1$s, has%2$s, %1$sOffset, %1$sLength, %3$d, " +
                        CODEC_VALIDATION_ENABLED + ");\n",
                    fieldName,
                    field.name(),
                    field.number());

            case INT:
            case LENGTH:
            case SEQNUM:
            case DAYOFMONTH:
                return lengthBasedFieldLazyInitialization(fieldName, "getIntFlyweight(buffer",
                    ", " + tag + ", " + CODEC_VALIDATION_ENABLED);

            case LONG:
                return lengthBasedFieldLazyInitialization(fieldName, "getLongFlyweight(buffer",
                    ", " + tag + ", " + CODEC_VALIDATION_ENABLED);

            case FLOAT:
            case PRICE:
            case PRICEOFFSET:
            case QTY:
            case QUANTITY:
            case PERCENTAGE:
            case AMT:
                return lengthBasedFieldLazyInitialization(fieldName, "getFloatFlyweight(buffer, " +
                    fieldName, ", " + tag + ", " + CODEC_VALIDATION_ENABLED);

            case STRING:
            case MULTIPLEVALUESTRING:
            case MULTIPLESTRINGVALUE:
            case MULTIPLECHARVALUE:
            case CURRENCY:
            case EXCHANGE:
            case COUNTRY:
            case LANGUAGE:
                return lengthBasedFieldLazyInitialization(fieldName, "buffer.getChars(" + fieldName, "");

            case DATA:
            case XMLDATA:
                // Length extracted separately from a preceding field
                final Field associatedLengthField = field.associatedLengthField();
                if (associatedLengthField == null)
                {
                    throw new IllegalStateException("No associated length field for: " + field);
                }
                final String associatedFieldName = formatPropertyName(associatedLengthField.name());
                return String.format(
                    "        if (buffer != null && %2$s > 0)\n" +
                    "        {\n" +
                    "            %1$s = buffer.getBytes(%1$s, %1$sOffset, %2$s);\n" +
                    "        }\n",
                    fieldName,
                    associatedFieldName);

            case UTCTIMESTAMP:
            case LOCALMKTDATE:
            case UTCTIMEONLY:
            case UTCDATEONLY:
            case TZTIMEONLY:
            case TZTIMESTAMP:
            case MONTHYEAR:
                return lengthBasedFieldLazyInitialization(fieldName, "buffer.getBytes(" + fieldName, "");

            case BOOLEAN:
            case CHAR:
            default:
                return "";
        }
    }

    private static String lengthBasedFieldLazyInitialization(
        final String fieldName, final String decodeMethod, final String endArgs)
    {
        return String.format(
            "        if (buffer != null && %1$sLength > 0)\n" +
            "        {\n" +
            "            %1$s = %2$s, %1$sOffset, %1$sLength%3$s);\n" +
            "        }\n",
            fieldName,
            decodeMethod,
            endArgs);
    }

    private String fieldInitialisation(final Type type)
    {
        switch (type)
        {
            case STRING:
            case MULTIPLEVALUESTRING:
            case MULTIPLESTRINGVALUE:
            case MULTIPLECHARVALUE:
            case CURRENCY:
            case EXCHANGE:
            case COUNTRY:
            case LANGUAGE:
                return String.format(" = new char[%d]", initialBufferSize);

            case FLOAT:
            case PRICE:
            case PRICEOFFSET:
            case QTY:
            case QUANTITY:
            case PERCENTAGE:
            case AMT:
                return " = DecimalFloat.newNaNValue()";

            case BOOLEAN:
                return "";

            case CHAR:
                return " = MISSING_CHAR";

            case INT:
            case LENGTH:
            case SEQNUM:
            case NUMINGROUP:
            case DAYOFMONTH:
                return " = MISSING_INT";

            case LONG:
                return " = MISSING_LONG";

            case DATA:
            case XMLDATA:
                return initByteArray(initialBufferSize);

            case UTCTIMESTAMP:
                return initByteArray(UtcTimestampDecoder.LENGTH_WITH_MICROSECONDS);

            case LOCALMKTDATE:
                return initByteArray(LocalMktDateDecoder.LENGTH);

            case UTCTIMEONLY:
                return initByteArray(UtcTimeOnlyDecoder.LONG_LENGTH);

            case UTCDATEONLY:
                return initByteArray(UtcDateOnlyDecoder.LENGTH);

            case MONTHYEAR:
                return initByteArray(MonthYear.LONG_LENGTH);

            case TZTIMEONLY:
                return initByteArray(UtcTimeOnlyDecoder.LONG_LENGTH + 7);

            case TZTIMESTAMP:
                return initByteArray(UtcTimestampDecoder.LENGTH_WITH_MICROSECONDS + 7);

            default:
                throw new UnsupportedOperationException("Unknown type: " + type);
        }
    }

    private String initByteArray(final int initialBufferSize)
    {
        return String.format(" = new byte[%d]", initialBufferSize);
    }

    private String optionalGetter(final Entry entry)
    {
        return entry.required() ? "" : hasGetter(entry.name());
    }

    private String optionalCheck(final Entry entry)
    {
        return entry.required() ? "" : String.format(
            "        if (!has%s)\n" +
            "        {\n" +
            "            throw new IllegalArgumentException(\"No value for optional field: %1$s\");\n" +
            "        }\n\n",
            entry.name());
    }

    private String javaTypeOf(final Type type)
    {
        switch (type)
        {
            case INT:
            case LENGTH:
            case SEQNUM:
            case NUMINGROUP:
            case DAYOFMONTH:
                return "int";

            case LONG:
                return "long";

            case FLOAT:
            case PRICE:
            case PRICEOFFSET:
            case QTY:
            case QUANTITY:
            case PERCENTAGE:
            case AMT:
                return "DecimalFloat";

            case CHAR:
                return "char";

            case STRING:
            case MULTIPLEVALUESTRING:
            case MULTIPLESTRINGVALUE:
            case MULTIPLECHARVALUE:
            case CURRENCY:
            case EXCHANGE:
            case COUNTRY:
            case LANGUAGE:
                return "char[]";

            case BOOLEAN:
                return "boolean";

            case DATA:
            case XMLDATA:
            case UTCTIMESTAMP:
            case UTCTIMEONLY:
            case UTCDATEONLY:
            case MONTHYEAR:
            case LOCALMKTDATE:
            case TZTIMEONLY:
            case TZTIMESTAMP:
                return "byte[]";

            default:
                throw new UnsupportedOperationException("Unknown type: " + type);
        }
    }

    private String decodeMethod(final List entries, final Aggregate aggregate, final AggregateType type)
    {
        if (isSharedParent())
        {
            return "";
        }

        final boolean hasCommonCompounds = type == MESSAGE;
        final boolean isGroup = type == GROUP;
        final boolean isHeader = type == HEADER;
        final String endGroupCheck = endGroupCheck(aggregate, isGroup);
        final String prefix = generateDecodePrefix(aggregate, hasCommonCompounds, isGroup, isHeader, endGroupCheck);
        final String body = entries.stream()
            .map(this::decodeEntry)
            .collect(joining("\n", "", "\n"));

        final String suffix =
            "            default:\n" +
            "                if (!" + CODEC_REJECT_UNKNOWN_FIELD_ENABLED + ")\n" +
            "                {\n" +
            (isGroup ?
            "                    seenFields.remove(tag);\n" :
            "                    alreadyVisitedFields.remove(tag);\n") +
            "                }\n" +
            (isGroup ? "" :
            "                else\n" +
            "                {\n" +
            "                    if (!" + unknownFieldPredicate(type) + ")\n" +
            "                    {\n" +
            "                        unknownFields.add(tag);\n" +
            "                    }\n" +
            "                }\n") +

            // Skip the thing if it's a completely unknown field and you aren't validating messages
            "                if (" + CODEC_REJECT_UNKNOWN_FIELD_ENABLED +
            " || " + unknownFieldPredicate(type) + ")\n" +
            "                {\n" +
            decodeTrailerOrReturn(hasCommonCompounds, 5) +
            "                }\n" +
            "\n" +
            "            }\n\n" +
            "            if (position < (endOfField + 1))\n" +
            "            {\n" +
            "                position = endOfField + 1;\n" +
            "            }\n" +
            "        }\n" +
            decodeTrailerOrReturn(hasCommonCompounds, 2) +
            "    }\n\n";
        return prefix + body + suffix;
    }

    private String generateDecodePrefix(
        final Aggregate aggregate,
        final boolean hasCommonCompounds,
        final boolean isGroup,
        final boolean isHeader,
        final String endGroupCheck)
    {
        return "    public int decode(final AsciiBuffer buffer, final int offset, final int length)\n" +
            "    {\n" +
            "        // Decode " + aggregate.name() + "\n" +
            "        int seenFieldCount = 0;\n" +
            "        if (" + CODEC_VALIDATION_ENABLED + ")\n" +
            "        {\n" +
            "            missingRequiredFields.copy(" + REQUIRED_FIELDS + ");\n" +
            (isGroup ? "" : "            alreadyVisitedFields.clear();\n") +
            "        }\n" +
            "        this.buffer = buffer;\n" +
            "        final int end = offset + length;\n" +
            "        int position = offset;\n" +
            (hasCommonCompounds ? "        position += header.decode(buffer, position, length);\n" : "") +
            (isGroup ? "        seenFields.clear();\n" : "") +
            "        int tag;\n\n" +
            "        while (position < end)\n" +
            "        {\n" +
            "            final int equalsPosition = buffer.scan(position, end, '=');\n" +
            "            if (equalsPosition == AsciiBuffer.UNKNOWN_INDEX)\n" +
            "            {\n" +
            "               return position;\n" +
            "            }\n" +
            "            tag = buffer.getInt(position, equalsPosition);\n" +
            endGroupCheck +
            "            final int valueOffset = equalsPosition + 1;\n" +
            "            int endOfField = buffer.scan(valueOffset, end, START_OF_HEADER);\n" +
            "            if (endOfField == AsciiBuffer.UNKNOWN_INDEX)\n" +
            "            {\n" +
            "                rejectReason = " + VALUE_IS_INCORRECT + ";\n" +
            "                break;\n" +
            "            }\n" +
            "            final int valueLength = endOfField - valueOffset;\n" +
            "            if (" + CODEC_VALIDATION_ENABLED + ")\n" +
            "            {\n" +
            "                if (tag <= 0)\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + INVALID_TAG_NUMBER + ";\n" +
            "                }\n" +
            "                else if (valueLength == 0)\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + TAG_SPECIFIED_WITHOUT_A_VALUE + ";\n" +
            "                }\n" +
            headerValidation(isHeader) +
            (isGroup ? "" :
            "                if (!alreadyVisitedFields.add(tag))\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + TAG_APPEARS_MORE_THAN_ONCE + ";\n" +
            "                }\n") +
            "                missingRequiredFields.remove(tag);\n" +
            "                seenFieldCount++;\n" +
            "            }\n\n" +
            "            switch (tag)\n" +
            "            {\n";
    }

    private String decodeTrailerOrReturn(final boolean hasCommonCompounds, final int indent)
    {
        return (hasCommonCompounds ?
            indent(indent, "position += trailer.decode(buffer, position, end - position);\n") : "") +
            indent(indent, "return position - offset;\n");
    }

    private String unknownFieldPredicate(final AggregateType type)
    {
        switch (type)
        {
            case TRAILER:
                return REQUIRED_FIELDS + ".contains(tag)";
            case HEADER:
                //HeaderDecoder has the special requirement of being used independently so cannot use context
                // of message to determine if field is unknown
                return "true";
            case MESSAGE:
                return "(trailer." + REQUIRED_FIELDS + ".contains(tag))";
            default:
                return "(trailer." + REQUIRED_FIELDS + ".contains(tag) || " + MESSAGE_FIELDS + ".contains(tag))";
        }
    }

    private String endGroupCheck(final Aggregate aggregate, final boolean isGroup)
    {
        final String endGroupCheck;
        if (isGroup)
        {
            endGroupCheck = String.format(
                "            if (!seenFields.add(tag))\n" +
                "            {\n" +
                "                if (next == null)\n" +
                "                {\n" +
                "                    next = new %1$s(trailer, %2$s);\n" +
                "                }\n" +
                "                return position - offset;\n" +
                "            }\n",
                decoderClassName(aggregate),
                MESSAGE_FIELDS);
        }
        else
        {
            endGroupCheck = "";
        }

        return endGroupCheck;
    }

    private String headerValidation(final boolean isHeader)
    {
        return isHeader ?
            "                else if (seenFieldCount == 0 && tag != 8)\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER + ";\n" +
            "                }\n" +
            "                else if (seenFieldCount == 1 && tag != 9)\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER + ";\n" +
            "                }\n" +
            "                else if (seenFieldCount == 2 && tag != 35)\n" +
            "                {\n" +
            "                    invalidTagId = tag;\n" +
            "                    rejectReason = " + TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER + ";\n" +
            "                }\n" :
            "";
    }

    private String decodeEntry(final Entry entry)
    {
        return entry.matchEntry(
            (e) -> decodeField(e, ""),
            this::decodeGroup,
            this::decodeComponent);
    }

    private String decodeComponent(final Entry entry)
    {
        final Component component = (Component)entry.element();
        return component
            .entries()
            .stream()
            .map(this::decodeEntry)
            .collect(joining("\n", "", "\n"));
    }

    protected String componentAppendTo(final Component component)
    {
        return component
            .entries()
            .stream()
            .map(this::generateEntryAppendTo)
            .collect(joining("\n"));
    }

    private String decodeGroup(final Entry entry)
    {
        final Group group = (Group)entry.element();

        final Entry numberField = group.numberField();
        final String groupNumberField = formatPropertyName(numberField.name());
        final String getNumberField;
        if (flyweightsEnabled)
        {
            // Pass missing int here to force re-read as we're in the decode method and don't want a stale cached value
            getNumberField = String.format(
                "this.%1$s = groupNoField(buffer, MISSING_INT, has%2$s, %1$sOffset, %1$sLength, %3$d, " +
                CODEC_VALIDATION_ENABLED + ")",
                groupNumberField,
                numberField.name(),
                numberField.number());
        }
        else
        {
            getNumberField = "this." + groupNumberField;
        }

        final String parseGroup = String.format(
            "                if (%1$s == null)\n" +
            "                {\n" +
            "                    %1$s = new %2$s(trailer, %5$s);\n" +
            "                }\n" +
            "                %2$s %1$sCurrent = %1$s;\n" +
            "                position = endOfField + 1;\n" +
            "                final int %3$s = %4$s;\n" +
            "                for (int i = 0; i < %3$s && position < end; i++)\n" +
            "                {\n" +
            "                    if (%1$sCurrent != null)\n" +
            "                    {\n" +
            "                        position += %1$sCurrent.decode(buffer, position, end - position);\n" +
            "                        %1$sCurrent = %1$sCurrent.next();\n" +
            "                    }\n" +
            "                }\n" +
            "                if (" + CODEC_VALIDATION_ENABLED + ")\n" +
            "                {\n" +
            "                    final int checkEqualsPosition = buffer.scan(position, end, '=');\n" +
            "                    if (checkEqualsPosition != AsciiBuffer.UNKNOWN_INDEX)\n" +
            "                    {\n" +
            "                        final int checkTag = buffer.getInt(position, checkEqualsPosition);\n" +
            "                        if (%1$s." + ALL_GROUP_FIELDS + ".contains(checkTag))\n" +
            "                        {\n" +
            "                            invalidTagId = tag;\n" +
            "                            rejectReason = %6$s;\n" +
            "                            return position;\n" +
            "                        }\n" +
            "                    }\n" +
            "                }\n",
            formatPropertyName(group.name()),
            decoderClassName(group),
            groupNumberField,
            // Have to make a call to initialise the group number at this point when flyweighting.
            getNumberField,
            MESSAGE_FIELDS,
            INCORRECT_NUMINGROUP_COUNT_FOR_REPEATING_GROUP);

        return decodeField(group.numberField(), parseGroup);
    }

    private String decodeField(final Entry entry, final String suffix)
    {
        // Uses variables from surrounding context:
        // int tag = the tag number of the field
        // int valueOffset = starting index of the value
        // int valueLength = the number of bytes for the value
        // int endOfField = the end index of the value

        final Field field = (Field)entry.element();
        final String name = entry.name();
        final String fieldName = formatPropertyName(name);

        return String.format(
            "            case Constants.%s:\n" +
            "%s" +
            "%s" +
            "%s" +
            "%s" +
            "%s" +
            "                break;\n",
            constantName(name),
            optionalAssign(entry),
            fieldDecodeMethod(field, fieldName),
            storeOffsetForVariableLengthFields(field.type(), fieldName),
            storeLengthForVariableLengthFields(field.type(), fieldName),
            suffix);
    }

    private String storeLengthForVariableLengthFields(final Type type, final String fieldName)
    {
        return type.hasLengthField(flyweightsEnabled) ?
            String.format("                %sLength = valueLength;\n", fieldName) :
            "";
    }

    private String storeOffsetForVariableLengthFields(final Type type, final String fieldName)
    {
        return type.hasOffsetField(flyweightsEnabled) ?
            String.format("                %sOffset = valueOffset;\n", fieldName) :
            "";
    }

    private String optionalAssign(final Entry entry)
    {
        return entry.required() ? "" : String.format("                has%s = true;\n", entry.name());
    }

    private String fieldDecodeMethod(final Field field, final String fieldName)
    {
        final String prefix = String.format("                %s = ", fieldName);
        final String decodeMethod;
        switch (field.type())
        {
            case INT:
            case LENGTH:
            case SEQNUM:
            case NUMINGROUP:
            case DAYOFMONTH:
                if (flyweightsEnabled)
                {
                    return "";
                }
                decodeMethod = String.format(
                    "getInt(buffer, valueOffset, endOfField, %d, " + CODEC_VALIDATION_ENABLED + ")", field.number());
                break;
            case LONG:
                if (flyweightsEnabled)
                {
                    return "";
                }
                decodeMethod = String.format(
                    "getLong(buffer, valueOffset, endOfField, %d, " + CODEC_VALIDATION_ENABLED + ")", field.number());
                break;
            case FLOAT:
            case PRICE:
            case PRICEOFFSET:
            case QTY:
            case QUANTITY:
            case PERCENTAGE:
            case AMT:
                if (flyweightsEnabled)
                {
                    return "";
                }
                decodeMethod = String.format(
                    "getFloat(buffer, %s, valueOffset, valueLength, %d, " + CODEC_VALIDATION_ENABLED + ")",
                    fieldName, field.number());
                break;
            case CHAR:
                decodeMethod = "buffer.getChar(valueOffset)";
                break;
            case STRING:
            case MULTIPLEVALUESTRING:
            case MULTIPLESTRINGVALUE:
            case MULTIPLECHARVALUE:
            case CURRENCY:
            case EXCHANGE:
            case COUNTRY:
            case LANGUAGE:
                if (flyweightsEnabled)
                {
                    return "";
                }
                decodeMethod = String.format("buffer.getChars(%s, valueOffset, valueLength)", fieldName);
                break;

            case BOOLEAN:
                decodeMethod = "buffer.getBoolean(valueOffset)";
                break;
            case DATA:
            case XMLDATA:
                // Length extracted separately from a preceding field
                final String associatedFieldName = formatPropertyName(field.associatedLengthField().name());
                if (flyweightsEnabled)
                {
                    return String.format(
                        "                endOfField = valueOffset + this.%1$s();\n", associatedFieldName);
                }
                return String.format(
                    "                %1$s = buffer.getBytes(%1$s, valueOffset, %2$s);\n" +
                    "                endOfField = valueOffset + %2$s;\n",
                    fieldName,
                    associatedFieldName);
            case UTCTIMESTAMP:
            case LOCALMKTDATE:
            case UTCTIMEONLY:
            case UTCDATEONLY:
            case TZTIMEONLY:
            case TZTIMESTAMP:
            case MONTHYEAR:
                if (flyweightsEnabled)
                {
                    return "";
                }
                decodeMethod = String.format("buffer.getBytes(%s, valueOffset, valueLength)", fieldName);
                break;

            default:
                throw new UnsupportedOperationException("Unknown type: " + field.type() + " in " + fieldName);
        }
        return prefix + decodeMethod + ";\n";
    }

    protected String stringAppendTo(final String fieldName)
    {
        return String.format("builder.append(this.%1$s(), 0, %1$sLength())", fieldName);
    }

    protected String dataAppendTo(final Field field, final String fieldName)
    {
        final String lengthName = formatPropertyName(field.associatedLengthField().name());

        if (flyweightsEnabled)
        {
            return String.format("appendData(builder, this.%1$s(), this.%2$s())", fieldName, lengthName);
        }

        return String.format("appendData(builder, %1$s, %2$s)", fieldName, lengthName);
    }

    protected String timeAppendTo(final String fieldName)
    {
        if (flyweightsEnabled)
        {
            return String.format("appendData(builder, this.%1$s(), %1$sLength())", fieldName);
        }

        return String.format("appendData(builder, %1$s, %1$sLength)", fieldName);
    }

    protected boolean hasFlag(final Entry entry, final Field field)
    {
        return !entry.required();
    }

    protected String resetTemporalValue(final String name)
    {
        return resetNothing(name);
    }

    protected String resetComponents(final List entries, final StringBuilder methods)
    {
        return entries
            .stream()
            .filter(Entry::isComponent)
            .map((entry) -> resetEntries(((Component)entry.element()).entries(), methods))
            .collect(joining());
    }

    protected String resetStringBasedData(final String name)
    {
        return String.format(
            "    public void %1$s()\n" +
                    "    {\n" +
                    "        %2$sOffset = 0;\n" +
                    "        %2$sLength = 0;\n" +
                    "    }\n\n",
            nameOfResetMethod(name),
            formatPropertyName(name));
    }

    protected String groupEntryAppendTo(final Group group, final String name)
    {
        if (isSharedParent())
        {
            return "";
        }

        final String numberField = group.numberField().name();
        return String.format(
            "        if (has%2$s)\n" +
            "        {\n" +
            "            indent(builder, level);\n" +
            "            builder.append(\"\\\"%1$s\\\": [\\n\");\n" +
            "            %3$s %4$s = this.%4$s;\n" +
            "            for (int i = 0, size = this.%5$s; i < size; i++)\n" +
            "            {\n" +
            "                indent(builder, level);\n" +
            "                %4$s.appendTo(builder, level + 1);\n" +
            "                if (%4$s.next() != null)\n" +
            "                {\n" +
            "                    builder.append(',');\n" +
            "                }\n" +
            "                builder.append('\\n');\n" +
            "                %4$s = %4$s.next();" +
            "            }\n" +
            "            indent(builder, level);\n" +
            "            builder.append(\"],\\n\");\n" +
            "        }\n",
            name,
            numberField,
            decoderClassName(name),
            formatPropertyName(name),
            formatPropertyName(numberField));
    }

    private String generateToEncoder(final Aggregate aggregate)
    {
        final String entriesToEncoder = aggregate
            .entries()
            .stream()
            .map(this::generateEntryToEncoder)
            .collect(joining("\n"));
        final String name = aggregate.name();

        String encoderClassName = encoderClassName(name);
        // Resolve ambiguous name with inherited parent aggregate
        if (aggregate instanceof Group)
        {
            encoderClassName = encoderClassName(parentAggregate().name()) + "." + encoderClassName;
        }

        return String.format(
            "    /**\n" +
            "     * {@inheritDoc}\n" +
            "     */\n" +
            "    public %1$s toEncoder(final Encoder encoder)\n" +
            "    {\n" +
            "        return toEncoder((%1$s)encoder);\n" +
            "    }\n\n" +
            "    public %1$s toEncoder(final %1$s encoder)\n" +
            "    {\n" +
            "        encoder.reset();\n" +
            "%2$s" +
            "        return encoder;\n" +
            "    }\n\n",
            encoderClassName,
            entriesToEncoder);
    }

    private String generateEntryToEncoder(final Entry entry)
    {
        return generateEntryToEncoder(entry, "encoder");
    }

    private String generateEntryToEncoder(final Entry entry, final String encoderName)
    {
        if (isBodyLength(entry))
        {
            return "";
        }

        final Entry.Element element = entry.element();
        final String name = entry.name();
        if (element instanceof Field)
        {
            final Field field = (Field)element;

            if (appendToChecksHasGetter(entry, field))
            {
                return String.format(
                    "        if (has%1$s())\n" +
                    "        {\n" +
                    "%2$s\n" +
                    "        }\n",
                    name,
                    indentedFieldToEncoder(encoderName, field, "            "));
            }
            else
            {
                return indentedFieldToEncoder(encoderName, field, "        ");
            }
        }
        else if (element instanceof Group)
        {
            return groupEntryToEncoder((Group)element, name, encoderName);
        }
        else if (element instanceof Component)
        {
            return componentToEncoder((Component)element, encoderName);
        }

        return "";
    }

    private String indentedFieldToEncoder(final String encoderName, final Field field, final String replacement)
    {
        final String fieldToEncoder = fieldToEncoder(field, encoderName);
        return NEWLINE
            .matcher(fieldToEncoder)
            .replaceAll(replacement);
    }

    protected String groupEntryToEncoder(final Group group, final String name, final String encoderName)
    {
        if (isSharedParent())
        {
            return "";
        }

        final String numberField = group.numberField().name();

        return String.format(
            "        if (has%1$s)\n" +
            "        {\n" +
            "            final int size = this.%4$s;\n" +
            "            %2$s %3$s = this.%3$s;\n" +
            "            %6$s %3$sEncoder = %5$s.%3$s(size);\n" +
            "            for (int i = 0; i < size; i++)\n" +
            "            {\n" +
            "                if (%3$s != null)\n" +
            "                {\n" +
            "                    %3$s.toEncoder(%3$sEncoder);\n" +
            "                    %3$s = %3$s.next();\n" +
            "                    %3$sEncoder = %3$sEncoder.next();\n" +
            "                }\n" +
            "            }\n" +
            "        }\n",
            numberField,
            decoderClassName(name),
            formatPropertyName(name),
            formatPropertyName(numberField),
            encoderName,
            encoderClassName(name));
    }

    protected String componentToEncoder(final Component component, final String encoderName)
    {
        final String name = component.name();
        final String varName = formatPropertyName(name);

        final String prefix = String.format(
            "\n        final %1$s %2$s = %3$s.%2$s();",
            encoderClassName(name),
            varName,
            encoderName);

        return component
            .entries()
            .stream()
            .map((entry) -> generateEntryToEncoder(entry, varName))
            .collect(joining("\n",
                prefix,
                "\n"));
    }

    private String fieldToEncoder(final Field field, final String encoderName)
    {
        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 String.format("%2$s.%1$s(this.%1$s(), 0, %1$sLength());", fieldName, encoderName);

            case UTCTIMEONLY:
            case UTCDATEONLY:
            case UTCTIMESTAMP:
            case LOCALMKTDATE:
            case MONTHYEAR:
            case TZTIMEONLY:
            case TZTIMESTAMP:
                return String.format("%2$s.%1$sAsCopy(this.%1$s(), 0, %1$sLength());", fieldName, encoderName);

            case FLOAT:
            case PRICE:
            case PRICEOFFSET:
            case QTY:
            case QUANTITY:
            case PERCENTAGE:
            case AMT:

            case INT:
            case LENGTH:
            case BOOLEAN:
            case CHAR:
            case SEQNUM:
            case DAYOFMONTH:
            case LONG:
                return String.format("%2$s.%1$s(this.%1$s());", fieldName, encoderName);

            case DATA:
            case XMLDATA:
                final String lengthName = formatPropertyName(field.associatedLengthField().name());

                return String.format(
                    "%3$s.%1$sAsCopy(this.%1$s(), 0, %2$s());%n%3$s.%2$s(this.%2$s());",
                    fieldName, lengthName, encoderName);

            case NUMINGROUP:
                // Deliberately blank since it gets set by the group ToEncoder logic.
            default:
                return "";
        }
    }

    protected String optionalReset(final Field field, final String name)
    {
        return resetByFlag(name);
    }

    protected boolean appendToChecksHasGetter(final Entry entry, final Field field)
    {
        return hasFlag(entry, field);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy