From 45e38940f224ce17dd57c54b50db0e2c2642051d Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Thu, 28 May 2026 16:35:54 -0400 Subject: [PATCH] fixedrecord support --- .../hlaxapi/HLADecoderRegistry.java | 74 ++++++++++++++++++- .../hlaxapi/HLADecoderRegistryTest.java | 57 ++++++++++++++ .../hlaxapi/HLAEncodingTestSupport.java | 41 ++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/yetanalytics/hlaxapi/HLADecoderRegistry.java b/src/main/java/com/yetanalytics/hlaxapi/HLADecoderRegistry.java index 2c80655..1f66f18 100644 --- a/src/main/java/com/yetanalytics/hlaxapi/HLADecoderRegistry.java +++ b/src/main/java/com/yetanalytics/hlaxapi/HLADecoderRegistry.java @@ -16,6 +16,7 @@ import hla.rti1516e.encoding.EncoderFactory; import hla.rti1516e.encoding.HLAboolean; import hla.rti1516e.encoding.HLAfixedArray; +import hla.rti1516e.encoding.HLAfixedRecord; import hla.rti1516e.encoding.HLAfloat32BE; import hla.rti1516e.encoding.HLAfloat32LE; import hla.rti1516e.encoding.HLAfloat64BE; @@ -106,6 +107,25 @@ public void registerFixedArray(String hlaType, String elementHlaType, int si registerElementBacked(hlaType, List.class, fixedArrayAdapter(elementAdapter, size)); } + /** + * Registers an HLAfixedRecord decoder. Decoded records are returned as + * immutable Map instances keyed by field name. The supplied map's iteration + * order is the encoded field order, so callers should pass an ordered map + * such as LinkedHashMap when field order matters. + */ + public void registerFixedRecord(String hlaType, Map fields) { + Objects.requireNonNull(fields, "fields"); + List fieldAdapters = new ArrayList(); + for (Map.Entry field : fields.entrySet()) { + String fieldName = normalizeFieldName(field.getKey()); + String fieldHlaType = field.getValue(); + RegisteredDecoder registeredField = lookupRegisteredDecoder(normalize(fieldHlaType)).resolved(this); + ElementAdapter fieldAdapter = requireElementAdapter(fieldHlaType, registeredField); + fieldAdapters.add(new FieldAdapter(fieldName, fieldAdapter)); + } + registerElementBacked(hlaType, Map.class, fixedRecordAdapter(fieldAdapters)); + } + public Set supportedTypes() { return Collections.unmodifiableSet(decoders.keySet()); } @@ -259,6 +279,31 @@ public Object extractValue(DataElement element) { }; } + private ElementAdapter fixedRecordAdapter(List fieldAdapters) { + List fields = List.copyOf(fieldAdapters); + return new ElementAdapter() { + @Override + public DataElement createElement() { + HLAfixedRecord record = encoderFactory.createHLAfixedRecord(); + for (FieldAdapter field : fields) { + record.add(field.elementAdapter().createElement()); + } + return record; + } + + @Override + public Object extractValue(DataElement element) { + HLAfixedRecord record = (HLAfixedRecord) element; + Map values = new LinkedHashMap(); + for (int index = 0; index < fields.size(); index++) { + FieldAdapter field = fields.get(index); + values.put(field.name(), field.elementAdapter().extractValue(record.get(index))); + } + return Collections.unmodifiableMap(values); + } + }; + } + private static List extractArrayValues(Iterable array, ElementAdapter elementAdapter) { List values = new ArrayList(); for (DataElement element : array) { @@ -270,7 +315,7 @@ private static List extractArrayValues(Iterable a private static ElementAdapter requireElementAdapter(String hlaType, RegisteredDecoder registered) { if (registered.elementAdapter() == null) { throw new IllegalArgumentException("HLA type " + normalize(hlaType) - + " is registered as a value decoder and cannot be used as an array element"); + + " is registered as a value decoder and cannot be used as an aggregate element"); } return registered.elementAdapter(); } @@ -318,6 +363,14 @@ private static String normalize(String hlaType) { return normalizedType; } + private static String normalizeFieldName(String fieldName) { + String normalizedFieldName = Objects.requireNonNull(fieldName, "fieldName").trim(); + if (normalizedFieldName.isEmpty()) { + throw new IllegalArgumentException("Fixed record field name must not be blank"); + } + return normalizedFieldName; + } + private static Class wrapperType(Class javaType) { if (!javaType.isPrimitive()) { return javaType; @@ -355,6 +408,25 @@ private interface ElementAdapter { Object extractValue(DataElement element); } + private static final class FieldAdapter { + + private final String name; + private final ElementAdapter elementAdapter; + + private FieldAdapter(String name, ElementAdapter elementAdapter) { + this.name = Objects.requireNonNull(name, "name"); + this.elementAdapter = Objects.requireNonNull(elementAdapter, "elementAdapter"); + } + + private String name() { + return name; + } + + private ElementAdapter elementAdapter() { + return elementAdapter; + } + } + private static final class RegisteredDecoder { private final String registeredType; diff --git a/src/test/java/com/yetanalytics/hlaxapi/HLADecoderRegistryTest.java b/src/test/java/com/yetanalytics/hlaxapi/HLADecoderRegistryTest.java index 961f295..a39aa94 100644 --- a/src/test/java/com/yetanalytics/hlaxapi/HLADecoderRegistryTest.java +++ b/src/test/java/com/yetanalytics/hlaxapi/HLADecoderRegistryTest.java @@ -2,6 +2,7 @@ import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.asciiString; import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.fixedArray; +import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.fixedRecord; import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.float32; import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.float64; import static com.yetanalytics.hlaxapi.HLAEncodingTestSupport.int16; @@ -16,7 +17,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.nio.ByteOrder; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -139,6 +142,44 @@ void decodesNestedRegisteredArrays() throws DecoderException { List.class)); } + @Test + void decodesRegisteredFixedRecords() throws DecoderException { + registry.registerAlias("AngleFloat64", "HLAfloat64BE"); + registry.registerFixedRecord("PositionRec", + fields("Lat", "AngleFloat64", "Long", "AngleFloat64")); + + assertEquals( + Map.of("Lat", 42.25d, "Long", -71.125d), + registry.decode( + "PositionRec", + fixedRecord( + float64(42.25d, ByteOrder.BIG_ENDIAN), + float64(-71.125d, ByteOrder.BIG_ENDIAN)), + Map.class)); + } + + @Test + void decodesArraysOfFixedRecords() throws DecoderException { + registry.registerFixedRecord("CoordinatePair", + fields("X", "HLAinteger16BE", "Y", "HLAinteger16BE")); + registry.registerVariableArray("CoordinateHistory", "CoordinatePair", Map.class); + + assertEquals( + List.of( + Map.of("X", (short) 1, "Y", (short) 2), + Map.of("X", (short) -3, "Y", (short) 4)), + registry.decode( + "CoordinateHistory", + variableArray( + fixedRecord( + int16((short) 1, ByteOrder.BIG_ENDIAN), + int16((short) 2, ByteOrder.BIG_ENDIAN)), + fixedRecord( + int16((short) -3, ByteOrder.BIG_ENDIAN), + int16((short) 4, ByteOrder.BIG_ENDIAN))), + List.class)); + } + @Test void rejectsUnknownTypes() { assertThrows(IllegalArgumentException.class, () -> registry.decoderFor("MissingHlaType")); @@ -171,6 +212,14 @@ void rejectsValueOnlyDecodersAsArrayElements() { () -> registry.registerVariableArray("CustomArray", "CustomValue", Object.class)); } + @Test + void rejectsValueOnlyDecodersAsFixedRecordFields() { + registry.register("CustomValue", Object.class, bytes -> new Object()); + + assertThrows(IllegalArgumentException.class, + () -> registry.registerFixedRecord("CustomRecord", Map.of("Value", "CustomValue"))); + } + static Stream builtInDecodings() { return Stream.of( Arguments.of("HLAboolean", new byte[] { 1 }, Boolean.TRUE), @@ -202,4 +251,12 @@ static Stream builtInDecodings() { Arguments.of("HLAASCIIstring", asciiString("xAPI Ready"), "xAPI Ready"), Arguments.of("HLAunicodeString", unicodeString("R\u00e9sum\u00e9"), "R\u00e9sum\u00e9")); } + + private static Map fields(String... namesAndTypes) { + LinkedHashMap fields = new LinkedHashMap(); + for (int index = 0; index < namesAndTypes.length; index += 2) { + fields.put(namesAndTypes[index], namesAndTypes[index + 1]); + } + return fields; + } } diff --git a/src/test/java/com/yetanalytics/hlaxapi/HLAEncodingTestSupport.java b/src/test/java/com/yetanalytics/hlaxapi/HLAEncodingTestSupport.java index 66a2f91..09e9a42 100644 --- a/src/test/java/com/yetanalytics/hlaxapi/HLAEncodingTestSupport.java +++ b/src/test/java/com/yetanalytics/hlaxapi/HLAEncodingTestSupport.java @@ -20,6 +20,7 @@ import hla.rti1516e.encoding.HLAboolean; import hla.rti1516e.encoding.HLAbyte; import hla.rti1516e.encoding.HLAfixedArray; +import hla.rti1516e.encoding.HLAfixedRecord; import hla.rti1516e.encoding.HLAfloat32BE; import hla.rti1516e.encoding.HLAfloat32LE; import hla.rti1516e.encoding.HLAfloat64BE; @@ -104,6 +105,7 @@ static EncoderFactory testEncoderFactory() { case "createHLAunicodeString" -> element(HLAunicodeString.class, unicodeStringDecoder(), initialValue); case "createHLAfixedArray" -> fixedArray(dataElementFactory(args[0]), (Integer) args[1]); + case "createHLAfixedRecord" -> fixedRecord(); case "createHLAvariableArray" -> variableArray(dataElementFactory(args[0])); default -> throw new UnsupportedOperationException(method.getName()); }; @@ -164,6 +166,10 @@ static byte[] fixedArray(byte[]... encodedElements) { return buffer.array(); } + static byte[] fixedRecord(byte[]... encodedFields) { + return fixedArray(encodedFields); + } + static byte[] variableArray(byte[]... encodedElements) { int size = Integer.BYTES; for (byte[] encodedElement : encodedElements) { @@ -250,6 +256,41 @@ private static HLAfixedArray fixedArray(DataElementFactory fields = new ArrayList(); + Object fixedRecord = Proxy.newProxyInstance( + HLAEncodingTestSupport.class.getClassLoader(), + new Class[] { HLAfixedRecord.class }, + (proxy, method, args) -> switch (method.getName()) { + case "decode" -> { + if (args != null && args.length == 1 && args[0] instanceof byte[] bytes) { + decodeComplete(bytes, byteWrapper -> decodeElements(byteWrapper, fields)); + yield null; + } + if (args != null && args.length == 1 && args[0] instanceof ByteWrapper byteWrapper) { + decodeElements(byteWrapper, fields); + yield null; + } + throw new UnsupportedOperationException("decode"); + } + case "add" -> { + fields.add((DataElement) args[0]); + yield null; + } + case "size" -> fields.size(); + case "get" -> fields.get((Integer) args[0]); + case "iterator" -> fields.iterator(); + case "getOctetBoundary" -> 1; + case "getEncodedLength" -> encodedLength(fields); + case "encode", "toByteArray" -> throw new UnsupportedOperationException(method.getName()); + case "toString" -> "HLAfixedRecord" + fields; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> throw new UnsupportedOperationException(method.getName()); + }); + return HLAfixedRecord.class.cast(fixedRecord); + } + private static HLAvariableArray variableArray(DataElementFactory factory) { List elements = new ArrayList(); Object variableArray = Proxy.newProxyInstance(