uk.co.real_logic.sbe.generation.common.FieldPrecedenceModel Maven / Gradle / Ivy
Show all versions of sbe-tool Show documentation
/*
* 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));
}
}
}
}