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

uk.co.real_logic.sbe.generation.common.FieldPrecedenceModel Maven / Gradle / Ivy

Go to download

FIX/SBE - OSI layer 6 presentation for encoding and decoding application messages in binary format for low-latency applications.

The newest version!
/*
 * Copyright 2013-2024 Real Logic Limited.
 *
 * 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.sbe.generation.common;

import uk.co.real_logic.sbe.generation.Generators;
import uk.co.real_logic.sbe.ir.Signal;
import uk.co.real_logic.sbe.ir.Token;
import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.collections.IntHashSet;
import org.agrona.collections.IntObjConsumer;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;

import static uk.co.real_logic.sbe.ir.GenerationUtil.collectFields;
import static uk.co.real_logic.sbe.ir.GenerationUtil.collectGroups;
import static uk.co.real_logic.sbe.ir.GenerationUtil.collectVarData;

/**
 * A state machine that models whether codec interactions are safe.
 */
@SuppressWarnings("CodeBlock2Expr")
// Lambdas without braces tend to conflict with the indentation Checkstyle expects.
// Therefore, we allow lambdas with code blocks even when a lambda expression is possible.
public final class FieldPrecedenceModel
{
    private final Map groupPathsByField = new HashMap<>();
    private final Set topLevelBlockFields = new HashSet<>();
    private final CodecInteraction.CodecInteractionFactory interactionFactory =
        new CodecInteraction.CodecInteractionFactory(groupPathsByField, topLevelBlockFields);
    private final Map> transitionsByInteraction = new LinkedHashMap<>();
    private final Map> transitionsByState = new HashMap<>();
    private final TreeMap versionWrappedStates = new TreeMap<>();
    private final State notWrappedState = allocateState("NOT_WRAPPED");
    private final String generatedRepresentationClassName;
    private State encoderWrappedState;
    private Set terminalEncoderStates;

    private FieldPrecedenceModel(final String generatedRepresentationClassName)
    {
        this.generatedRepresentationClassName = generatedRepresentationClassName;
    }

    /**
     * Builds a state machine that models whether codec interactions are safe.
     *
     * @param stateClassName the qualified name of the class that models the state machine in generated code
     * @param msgToken the message token
     * @param fields the fields in the message
     * @param groups the groups in the message
     * @param varData the varData in the message
     * @param versionsSelector a function that selects the versions to model in the state machine
     * @return the access order model
     */
    public static FieldPrecedenceModel newInstance(
        final String stateClassName,
        final Token msgToken,
        final List fields,
        final List groups,
        final List varData,
        final Function versionsSelector)
    {
        final FieldPrecedenceModel model = new FieldPrecedenceModel(stateClassName);
        model.findTransitions(msgToken, fields, groups, varData, versionsSelector);
        return model;
    }

    /**
     * The initial state before a codec is wrapped.
     * @return the initial state before a codec is wrapped.
     */
    public State notWrappedState()
    {
        return notWrappedState;
    }

    /**
     * The state after a codec is wrapped in the latest version.
     * @return the state after a codec is wrapped in the latest version.
     */
    public State latestVersionWrappedState()
    {
        return encoderWrappedState;
    }

    /**
     * Iterates over the states after a codec is wrapped over a particular version of data.
     * @param consumer the consumer of the states.
     */
    public void forEachWrappedStateByVersionDesc(final IntObjConsumer consumer)
    {
        for (final Map.Entry entry : versionWrappedStates.descendingMap().entrySet())
        {
            consumer.accept(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Returns the number of schema versions.
     * @return the number of schema versions.
     */
    public int versionCount()
    {
        return versionWrappedStates.size();
    }

    /**
     * Iterates over the states in which a codec is fully encoded.
     * @param consumer the consumer of the states.
     */
    public void forEachTerminalEncoderState(final Consumer consumer)
    {
        terminalEncoderStates.forEach(consumer);
    }

    /**
     * Iterates over the codec states in the order of their state numbers.
     * @param consumer the consumer of the states.
     */
    public void forEachStateOrderedByStateNumber(final Consumer consumer)
    {
        transitionsByState.keySet().stream()
            .sorted(Comparator.comparingInt((s) -> s.number))
            .forEach(consumer);
    }

    /**
     * Returns the number of states in the state machine.
     * @return the number of states in the state machine.
     */
    public int stateCount()
    {
        return transitionsByState.size();
    }

    /**
     * Returns a hash-consing factory for codec interactions.
     * These interactions are the transitions in the state machine.
     * @see CodecInteraction
     * @return a hash-consing factory for codec interactions.
     */
    public CodecInteraction.CodecInteractionFactory interactionFactory()
    {
        return interactionFactory;
    }

    /**
     * Returns the name of the class that models the state machine in generated code.
     * @return the name of the class that models the state machine in generated code.
     */
    public String generatedRepresentationClassName()
    {
        return generatedRepresentationClassName;
    }

    /**
     * Iterates over the possible state machine transitions due to the supplied codec interaction.
     * @param interaction a codec interaction.
     * @param consumer the consumer of the transitions.
     */
    public void forEachTransition(
        final CodecInteraction interaction,
        final Consumer consumer)
    {
        final List transitionsForContext = transitionsByInteraction.get(interaction);
        if (null != transitionsForContext)
        {
            transitionsForContext.forEach(consumer);
        }
    }

    /**
     * Finds the possible transitions from a given state.
     * @param state the state to find transitions from.
     * @param consumer the consumer of the transitions.
     */
    public void forEachTransitionFrom(final State state, final Consumer consumer)
    {
        final List transitionGroups = transitionsByState.get(state);
        if (null != transitionGroups)
        {
            transitionGroups.forEach(consumer);
        }
    }

    /**
     * Encodes the state machine as a graphviz dot diagram.
     * @param sb the string builder to append to.
     * @param indent the indentation to use.
     */
    @SuppressWarnings("SameParameterValue")
    public void generateGraph(final StringBuilder sb, final String indent)
    {
        sb.append(indent).append("digraph G {\n");
        transitionsByInteraction.values().forEach(
            (transitionsForContext) ->
            {
                transitionsForContext.forEach(
                    (transition) ->
                    {
                        transition.forEachStartState(
                            (startState) ->
                            {
                                sb.append(indent).append("    ")
                                    .append(startState.name)
                                    .append(" -> ")
                                    .append(transition.endState().name)
                                    .append(" [label=\"  ").append(transition.interaction.exampleCode());

                                if (!transition.interaction.exampleConditions().isEmpty())
                                {
                                    sb.append("\\n").append("  where ")
                                        .append(transition.interaction.exampleConditions());
                                }

                                sb.append("  \"];\n");
                            });
                    });
            });
        sb.append(indent).append("}\n");
    }

    private void findTransitions(
        final Token msgToken,
        final List fields,
        final List groups,
        final List varData,
        final Function versionsSelector)
    {
        final IntHashSet versions = new IntHashSet();
        versions.add(msgToken.version());
        walkSchemaLevel(new VersionCollector(versions), fields, groups, varData);
        walkSchemaLevel(new PathCollector(topLevelBlockFields, groupPathsByField), fields, groups, varData);

        final IntStream selectedVersions = versionsSelector.apply(versions.stream().mapToInt((i) -> i));
        selectedVersions.sorted().forEach(
            (version) ->
            {
                final State versionWrappedState = allocateState("V" + version + "_BLOCK");

                versionWrappedStates.put(version, versionWrappedState);

                final CodecInteraction wrapInteraction = interactionFactory.wrap(version);

                allocateTransitions(
                    wrapInteraction,
                    Collections.singletonList(notWrappedState),
                    versionWrappedState);

                final TransitionCollector transitionCollector = new TransitionCollector(
                    "V" + version + "_",
                    Collections.singleton(versionWrappedState),
                    versionWrappedState,
                    (token) -> token.version() <= version);

                walkSchemaLevel(transitionCollector, fields, groups, varData);

                // Last writer (highest version) wins when there are multiple versions
                encoderWrappedState = versionWrappedState;
                terminalEncoderStates = transitionCollector.exitStates();
            });
    }

    private State allocateState(final String name)
    {
        final State state = new State(transitionsByState.size(), name);
        transitionsByState.put(state, new ArrayList<>());
        return state;
    }

    private void allocateTransitions(
        final CodecInteraction interaction,
        final Collection from,
        final State to)
    {
        final TransitionGroup transitionGroup = new TransitionGroup(interaction, from, to);
        final List transitionsForInteraction = transitionsByInteraction.computeIfAbsent(
            interaction, (ignored) -> new ArrayList<>());

        final boolean duplicateEndState =
            transitionsForInteraction.stream().anyMatch((t) -> t.to.number == transitionGroup.to.number);

        if (duplicateEndState)
        {
            throw new IllegalStateException("Duplicate end state: " + transitionGroup.to.name);
        }

        final Optional conflictingTransition = transitionsForInteraction.stream()
            .filter((t) -> t.from.stream().anyMatch(transitionGroup.from::contains))
            .findAny();

        if (conflictingTransition.isPresent())
        {
            throw new IllegalStateException(
                "Conflicting transition: " + transitionGroup + " conflicts with " + conflictingTransition.get());
        }

        transitionsForInteraction.add(transitionGroup);

        from.forEach((fromState) -> transitionsByState.get(fromState).add(transitionGroup));
    }

    private static void walkSchemaLevel(
        final SchemaConsumer consumer,
        final List fields,
        final List groups,
        final List varData)
    {
        Generators.forEachField(fields, (token, ignored) -> consumer.onBlockField(token));

        for (int i = 0; i < groups.size(); i++)
        {
            final Token token = groups.get(i);
            if (token.signal() != Signal.BEGIN_GROUP)
            {
                throw new IllegalStateException("tokens must begin with BEGIN_GROUP: token=" + token);
            }

            ++i;
            final int groupHeaderTokenCount = groups.get(i).componentTokenCount();
            i += groupHeaderTokenCount;

            final ArrayList groupFields = new ArrayList<>();
            i = collectFields(groups, i, groupFields);
            final ArrayList groupGroups = new ArrayList<>();
            i = collectGroups(groups, i, groupGroups);
            final ArrayList groupVarData = new ArrayList<>();
            i = collectVarData(groups, i, groupVarData);

            consumer.onEnterRepeatingGroup(token, groupFields, groupGroups, groupVarData);
        }

        for (int i = 0; i < varData.size(); )
        {
            final Token token = varData.get(i);
            if (token.signal() != Signal.BEGIN_VAR_DATA)
            {
                throw new IllegalStateException("tokens must begin with BEGIN_VAR_DATA: token=" + token);
            }
            i += token.componentTokenCount();

            consumer.onVarData(token);
        }
    }

    private interface SchemaConsumer
    {
        void onBlockField(Token token);

        void onEnterRepeatingGroup(
            Token token,
            List groupFields,
            List groupGroups,
            List groupVarData);

        void onVarData(Token token);
    }

    private static final class PathCollector implements SchemaConsumer
    {
        private final ArrayDeque groupPath = new ArrayDeque<>();
        private final Set topLevelBlockFields;
        private final Map groupPathsByField;

        private PathCollector(
            final Set topLevelBlockFields,
            final Map groupPathsByField)
        {
            this.topLevelBlockFields = topLevelBlockFields;
            this.groupPathsByField = groupPathsByField;
        }

        @Override
        public void onBlockField(final Token token)
        {
            if (groupPath.isEmpty())
            {
                topLevelBlockFields.add(token);
            }

            groupPathsByField.put(token, currentGroupPath());
        }

        public void onEnterRepeatingGroup(
            final Token token,
            final List groupFields,
            final List groupGroups,
            final List groupVarData)
        {
            groupPathsByField.put(token, currentGroupPath());
            groupPath.addLast(token);
            walkSchemaLevel(this, groupFields, groupGroups, groupVarData);
            groupPath.removeLast();
        }

        public void onVarData(final Token token)
        {
            groupPathsByField.put(token, currentGroupPath());
        }

        private String currentGroupPath()
        {
            final StringBuilder sb = new StringBuilder();
            groupPath.forEach(
                (token) ->
                {
                    sb.append(token.name()).append('.');
                });
            return sb.toString();
        }
    }

    private static final class VersionCollector implements SchemaConsumer
    {
        private final IntHashSet versions;

        VersionCollector(final IntHashSet versions)
        {
            this.versions = versions;
        }

        public void onBlockField(final Token token)
        {
            versions.add(token.version());
        }

        public void onEnterRepeatingGroup(
            final Token token,
            final List groupFields,
            final List groupGroups,
            final List groupVarData)
        {
            versions.add(token.version());
            walkSchemaLevel(this, groupFields, groupGroups, groupVarData);
        }

        public void onVarData(final Token token)
        {
            versions.add(token.version());
        }
    }

    private final class TransitionCollector implements SchemaConsumer
    {
        private final String statePrefix;
        private final HashSet currentStates;
        private final State blockState;
        private final Predicate filter;

        private TransitionCollector(
            final String statePrefix,
            final Set fromStates,
            final State blockState,
            final Predicate filter)
        {
            this.statePrefix = statePrefix;
            this.currentStates = new HashSet<>(fromStates);
            this.blockState = blockState;
            this.filter = filter;

            currentStates.add(blockState);
        }

        public void onBlockField(final Token token)
        {
            if (filter.test(token))
            {
                final CodecInteraction codecInteraction = interactionFactory.accessField(token);
                allocateTransitions(codecInteraction, currentStates, blockState);
            }
        }

        public void onEnterRepeatingGroup(
            final Token token,
            final List groupFields,
            final List groupGroups,
            final List groupVarData)
        {
            if (filter.test(token))
            {
                final String groupName = token.name().toUpperCase();
                final String groupPrefix = statePrefix + groupName + "_";
                final State nRemainingGroup = allocateState(groupPrefix + "N");
                final State nRemainingGroupElement = allocateState(groupPrefix + "N_BLOCK");
                final State oneRemainingGroupElement = allocateState(groupPrefix + "1_BLOCK");
                final State doneGroup = allocateState(groupPrefix + "DONE");

                final Set beginGroupStates = new HashSet<>(currentStates);

                // fooCount(0)
                final CodecInteraction emptyGroupInteraction = interactionFactory.determineGroupIsEmpty(token);
                allocateTransitions(emptyGroupInteraction, beginGroupStates, doneGroup);

                // fooCount(N) where N > 0
                final CodecInteraction nonEmptyGroupInteraction = interactionFactory.determineGroupHasElements(token);
                allocateTransitions(nonEmptyGroupInteraction, beginGroupStates, nRemainingGroup);

                currentStates.clear();
                currentStates.add(nRemainingGroupElement);
                final TransitionCollector nRemainingCollector = new TransitionCollector(
                    groupPrefix + "N_",
                    currentStates,
                    nRemainingGroupElement,
                    filter);
                walkSchemaLevel(nRemainingCollector, groupFields, groupGroups, groupVarData);

                currentStates.clear();
                currentStates.add(nRemainingGroup);
                currentStates.addAll(nRemainingCollector.exitStates());

                // where more than one element remains in the group
                final CodecInteraction nextGroupElementInteraction = interactionFactory.moveToNextElement(token);
                allocateTransitions(nextGroupElementInteraction, currentStates, nRemainingGroupElement);

                currentStates.clear();
                currentStates.add(nRemainingGroup);
                currentStates.addAll(nRemainingCollector.exitStates());

                // where only one element remains in the group
                final CodecInteraction lastGroupElementInteraction = interactionFactory.moveToLastElement(token);
                allocateTransitions(lastGroupElementInteraction, currentStates, oneRemainingGroupElement);

                currentStates.clear();
                currentStates.add(oneRemainingGroupElement);

                final TransitionCollector oneRemainingCollector = new TransitionCollector(
                    groupPrefix + "1_",
                    currentStates,
                    oneRemainingGroupElement,
                    filter);
                walkSchemaLevel(oneRemainingCollector, groupFields, groupGroups, groupVarData);

                final CodecInteraction resetCountToIndexInteraction = interactionFactory.resetCountToIndex(token);
                currentStates.clear();
                currentStates.add(doneGroup);
                currentStates.add(nRemainingGroup);
                currentStates.addAll(nRemainingCollector.exitStates());
                currentStates.addAll(oneRemainingCollector.exitStates());
                allocateTransitions(resetCountToIndexInteraction, currentStates, doneGroup);

                currentStates.clear();
                currentStates.add(doneGroup);
                currentStates.addAll(oneRemainingCollector.exitStates());
            }
        }

        public void onVarData(final Token token)
        {
            if (filter.test(token))
            {
                final CodecInteraction lengthAccessInteraction = interactionFactory.accessVarDataLength(token);
                currentStates.forEach(
                    (state) -> allocateTransitions(lengthAccessInteraction, Collections.singleton(state), state));

                final CodecInteraction codecInteraction = interactionFactory.accessField(token);
                final State accessedState = allocateState(statePrefix + token.name().toUpperCase() + "_DONE");
                allocateTransitions(codecInteraction, currentStates, accessedState);
                currentStates.clear();
                currentStates.add(accessedState);
            }
        }

        private Set exitStates()
        {
            return currentStates;
        }
    }

    /**
     * A state in which a codec may reside.
     */
    public static final class State
    {
        private final int number;
        private final String name;

        private State(final int number, final String name)
        {
            this.number = number;
            this.name = name;
        }

        /**
         * In the scope of an {@code FieldPrecedenceModel} instance, state numbers are contiguous
         * and start at 0. This numbering scheme allows easy generation of lookup tables.
         * @return the state number
         */
        public int number()
        {
            return number;
        }

        /**
         * Returns the name of the state.
         * @return the name of the state.
         */
        public String name()
        {
            return name;
        }

        /**
         * {@inheritDoc}
         */
        public String toString()
        {
            return "State{" +
                "number=" + number +
                ", name='" + name + '\'' +
                '}';
        }
    }

    /**
     * A group of transitions that share the same end state.
     */
    public static final class TransitionGroup
    {
        private final CodecInteraction interaction;
        private final Set from;
        private final State to;

        private TransitionGroup(
            final CodecInteraction interaction,
            final Collection from,
            final State to)
        {
            this.interaction = interaction;
            this.from = new HashSet<>(from);
            this.to = to;
        }

        /**
         * Iterate over the start states of the transitions in this group.
         * @param consumer the consumer of the start states.
         */
        public void forEachStartState(final Consumer consumer)
        {
            from.forEach(consumer);
        }

        /**
         * Returns the end state of the transitions in this group.
         * @return the end state of the transitions in this group.
         */
        public State endState()
        {
            return to;
        }

        /**
         * Returns {@code true} if the transitions in this group do not change state.
         * @return {@code true} if the transitions in this group do not change state.
         */
        public boolean alwaysEndsInStartState()
        {
            return from.size() == 1 && from.contains(to);
        }

        /**
         * Returns some example code for the codec interaction that the transitions in this group share.
         * Useful for producing error messages.
         * @return some example code for the codec interaction that the transitions in this group share.
         */
        public String exampleCode()
        {
            return interaction.exampleCode();
        }

        /**
         * {@inheritDoc}
         */
        public String toString()
        {
            return "Transition{" +
                "interaction='" + exampleCode() + '\'' +
                ", from=" + from +
                ", to=" + to +
                '}';
        }
    }

    /**
     * Represents an interaction against a codec, e.g., {@code encoder.wrap(...)} or {@code decoder.myVarData()}.
     */
    public abstract static class CodecInteraction
    {
        /**
         * Returns a name for the interaction qualified by any groups that it is nested within.
         * @return a name for the interaction qualified by any groups that it is nested within.
         */
        public abstract String groupQualifiedName();

        abstract String exampleCode();

        abstract String exampleConditions();

        /**
         * Returns {@code true} if this interaction is a top-level block field access; {@code false} otherwise.
         * @return {@code true} if this interaction is a top-level block field access; {@code false} otherwise.
         */
        public final boolean isTopLevelBlockFieldAccess()
        {
            if (this instanceof AccessField)
            {
                final AccessField accessField = (AccessField)this;
                return accessField.isTopLevelBlockField();
            }

            return false;
        }

        /**
         * When an encoder or decoder is wrapped around a buffer.
         */
        private static final class Wrap extends CodecInteraction
        {
            private final int version;

            Wrap(final int version)
            {
                this.version = version;
            }

            public String groupQualifiedName()
            {
                return "wrap";
            }

            public String exampleCode()
            {
                return "wrap(version=" + version + ")";
            }

            public String exampleConditions()
            {
                return "";
            }
        }

        /**
         * When a block or variable-length field is accessed.
         */
        private static final class AccessField extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;
            private final boolean isTopLevelBlockField;

            private AccessField(final String groupPath, final Token token, final boolean isTopLevelBlockField)
            {
                this.isTopLevelBlockField = isTopLevelBlockField;
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_FIELD || token.signal() == Signal.BEGIN_VAR_DATA;
                this.groupPath = groupPath;
                this.token = token;
            }

            boolean isTopLevelBlockField()
            {
                return isTopLevelBlockField;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            public String exampleCode()
            {
                return groupPath + token.name() + "(?)";
            }

            public String exampleConditions()
            {
                return "";
            }
        }

        /**
         * When a repeating group count is supplied as zero.
         */
        private static final class DetermineGroupIsEmpty extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private DetermineGroupIsEmpty(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_GROUP;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + "Count(0)";
            }

            String exampleConditions()
            {
                return "";
            }
        }

        /**
         * When a repeating group count is supplied as greater than zero.
         */
        private static final class DetermineGroupHasElements extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private DetermineGroupHasElements(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_GROUP;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + "Count(>0)";
            }

            String exampleConditions()
            {
                return "";
            }
        }

        /**
         * When the next element in a repeating group is accessed,
         * and it is not the last element.
         */
        private static final class MoveToNextElement extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private MoveToNextElement(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_GROUP;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + ".next()";
            }

            String exampleConditions()
            {
                return "count - newIndex > 1";
            }
        }

        /**
         * When the next element in a repeating group is accessed,
         * and it is the last element.
         */
        private static final class MoveToLastElement extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private MoveToLastElement(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_GROUP;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + ".next()";
            }

            String exampleConditions()
            {
                return "count - newIndex == 1";
            }
        }

        /**
         * When the number of elements in a repeating group is set to be its current extent.
         */
        private static final class ResetCountToIndex extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private ResetCountToIndex(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_GROUP;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + ".resetCountToIndex()";
            }

            String exampleConditions()
            {
                return "";
            }
        }

        /**
         * When the length of a variable length field is accessed without adjusting the position.
         */
        private static final class AccessVarDataLength extends CodecInteraction
        {
            private final String groupPath;
            private final Token token;

            private AccessVarDataLength(final String groupPath, final Token token)
            {
                assert groupPath != null;
                assert token.signal() == Signal.BEGIN_VAR_DATA;
                this.groupPath = groupPath;
                this.token = token;
            }

            public String groupQualifiedName()
            {
                return groupPath + token.name();
            }

            String exampleCode()
            {
                return groupPath + token.name() + "Length()";
            }

            String exampleConditions()
            {
                return "";
            }
        }

        /**
         * Factory for creating {@link CodecInteraction} instances. This factory
         * is used to hash-cons the instances, so that they can be compared by
         * reference.
         */
        public static final class CodecInteractionFactory
        {
            private final Int2ObjectHashMap wrapInteractions = new Int2ObjectHashMap<>();
            private final Map accessFieldInteractions = new HashMap<>();
            private final Map determineGroupIsEmptyInteractions = new HashMap<>();
            private final Map determineGroupHasElementsInteractions = new HashMap<>();
            private final Map moveToNextElementInteractions = new HashMap<>();
            private final Map moveToLastElementInteractions = new HashMap<>();
            private final Map resetCountToIndexInteractions = new HashMap<>();
            private final Map accessVarDataLengthInteractions = new HashMap<>();
            private final Map groupPathsByField;
            private final Set topLevelBlockFields;

            CodecInteractionFactory(
                final Map groupPathsByField,
                final Set topLevelBlockFields)
            {

                this.groupPathsByField = groupPathsByField;
                this.topLevelBlockFields = topLevelBlockFields;
            }

            /**
             * Find or create a {@link CodecInteraction} to represent wrapping a codec around
             * the given version of data.
             * @param version the version of data to wrap
             * @return the {@link CodecInteraction} instance
             */
            public CodecInteraction wrap(final int version)
            {
                return wrapInteractions.computeIfAbsent(version, Wrap::new);
            }

            /**
             * Find or create a {@link CodecInteraction} to represent accessing the field identified
             * by the given token.
             *
             * 

The supplied token must carry a {@link Signal#BEGIN_FIELD} * or {@link Signal#BEGIN_VAR_DATA} signal. * * @param token the token identifying the field * @return the {@link CodecInteraction} instance */ public CodecInteraction accessField(final Token token) { return accessFieldInteractions.computeIfAbsent( token, (t) -> new AccessField(groupPathsByField.get(t), t, topLevelBlockFields.contains(t))); } /** * Find or create a {@link CodecInteraction} to represent determining a * repeating group is empty. * *

For encoding, this will be when the group count is supplied, e.g., * {@code encoder.myGroupCount(0)}. * *

For decoding, this will be when the group is read, e.g., * {@code decoder.myGroup()}. * *

The supplied token must carry a {@link Signal#BEGIN_GROUP} signal. * * @param token the token identifying the group * @return the {@link CodecInteraction} instance */ public CodecInteraction determineGroupIsEmpty(final Token token) { return determineGroupIsEmptyInteractions.computeIfAbsent( token, (t) -> new DetermineGroupIsEmpty(groupPathsByField.get(t), t)); } /** * Find or create a {@link CodecInteraction} to represent determining a * repeating group has elements. * *

For encoding, this will be when the group count is supplied, e.g., * {@code encoder.myGroupCount(1)}. * *

For decoding, this will be when the group is read, e.g., * {@code decoder.myGroup()}. * *

The supplied token must carry a {@link Signal#BEGIN_GROUP} signal. * * @param token the token identifying the group * @return the {@link CodecInteraction} instance */ public CodecInteraction determineGroupHasElements(final Token token) { return determineGroupHasElementsInteractions.computeIfAbsent( token, (t) -> new DetermineGroupHasElements(groupPathsByField.get(t), t)); } /** * Find or create a {@link CodecInteraction} to represent moving to the next * element in a repeating group. * *

For encoders, decoders, and codecs, this will be when the next element * is accessed, e.g., {@code myGroup.next()} when {@code myGroup.count - myGroup.index > 1}. * *

The supplied token must carry a {@link Signal#BEGIN_GROUP} signal. * * @param token the token identifying the group * @return the {@link CodecInteraction} instance */ public CodecInteraction moveToNextElement(final Token token) { return moveToNextElementInteractions.computeIfAbsent( token, (t) -> new MoveToNextElement(groupPathsByField.get(t), t)); } /** * Find or create a {@link CodecInteraction} to represent moving to the last * element in a repeating group. * *

For encoders, decoders, and codecs, this will be when the last element * is accessed, e.g., {@code myGroup.next()} when {@code myGroup.count - myGroup.index == 1}. * * @param token the token identifying the group * @return the {@link CodecInteraction} instance */ public CodecInteraction moveToLastElement(final Token token) { return moveToLastElementInteractions.computeIfAbsent( token, (t) -> new MoveToLastElement(groupPathsByField.get(t), t)); } /** * Find or create a {@link CodecInteraction} to represent resetting the count * of a repeating group to the current index. * *

For encoders, decoders, and codecs, this will be when the {@code resetCountToIndex} * method is called, e.g., {@code myGroup.resetCountToIndex()}. * *

The supplied token must carry a {@link Signal#BEGIN_GROUP} signal. * * @param token the token identifying the group * @return the {@link CodecInteraction} instance */ public CodecInteraction resetCountToIndex(final Token token) { return resetCountToIndexInteractions.computeIfAbsent( token, (t) -> new ResetCountToIndex(groupPathsByField.get(t), t)); } /** * Find or create a {@link CodecInteraction} to represent accessing the length * of a variable-length data field without advancing the codec position. * *

For decoders and codecs, this will be when the length is accessed, e.g., * {@code decoder.myVarDataLength()}. * *

The supplied token must carry a {@link Signal#BEGIN_VAR_DATA} signal. * * @param token the token identifying the field * @return the {@link CodecInteraction} instance */ public CodecInteraction accessVarDataLength(final Token token) { return accessVarDataLengthInteractions.computeIfAbsent( token, (t) -> new AccessVarDataLength(groupPathsByField.get(t), t)); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy