Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion src/main/java/com/yetanalytics/hlaxapi/HLADecoderRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,6 +107,25 @@ public <T> 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<String, String> fields) {
Objects.requireNonNull(fields, "fields");
List<FieldAdapter> fieldAdapters = new ArrayList<FieldAdapter>();
for (Map.Entry<String, String> 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<String> supportedTypes() {
return Collections.unmodifiableSet(decoders.keySet());
}
Expand Down Expand Up @@ -259,6 +279,31 @@ public Object extractValue(DataElement element) {
};
}

private ElementAdapter fixedRecordAdapter(List<FieldAdapter> fieldAdapters) {
List<FieldAdapter> 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<String, Object> values = new LinkedHashMap<String, Object>();
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<Object> extractArrayValues(Iterable<? extends DataElement> array, ElementAdapter elementAdapter) {
List<Object> values = new ArrayList<Object>();
for (DataElement element : array) {
Expand All @@ -270,7 +315,7 @@ private static List<Object> extractArrayValues(Iterable<? extends DataElement> 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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
57 changes: 57 additions & 0 deletions src/test/java/com/yetanalytics/hlaxapi/HLADecoderRegistryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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<Arguments> builtInDecodings() {
return Stream.of(
Arguments.of("HLAboolean", new byte[] { 1 }, Boolean.TRUE),
Expand Down Expand Up @@ -202,4 +251,12 @@ static Stream<Arguments> builtInDecodings() {
Arguments.of("HLAASCIIstring", asciiString("xAPI Ready"), "xAPI Ready"),
Arguments.of("HLAunicodeString", unicodeString("R\u00e9sum\u00e9"), "R\u00e9sum\u00e9"));
}

private static Map<String, String> fields(String... namesAndTypes) {
LinkedHashMap<String, String> fields = new LinkedHashMap<String, String>();
for (int index = 0; index < namesAndTypes.length; index += 2) {
fields.put(namesAndTypes[index], namesAndTypes[index + 1]);
}
return fields;
}
}
41 changes: 41 additions & 0 deletions src/test/java/com/yetanalytics/hlaxapi/HLAEncodingTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -250,6 +256,41 @@ private static HLAfixedArray<DataElement> fixedArray(DataElementFactory<DataElem
return typedArray;
}

private static HLAfixedRecord fixedRecord() {
List<DataElement> fields = new ArrayList<DataElement>();
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<DataElement> variableArray(DataElementFactory<DataElement> factory) {
List<DataElement> elements = new ArrayList<DataElement>();
Object variableArray = Proxy.newProxyInstance(
Expand Down