SerialDataDecoder.java
package org.djutils.serialization;
import java.io.IOException;
import org.djunits.unit.Unit;
import org.djutils.decoderdumper.Decoder;
import org.djutils.serialization.serializers.ArrayOrMatrixSerializer;
import org.djutils.serialization.serializers.BasicPrimitiveArrayOrMatrixSerializer;
import org.djutils.serialization.serializers.FixedSizeObjectSerializer;
import org.djutils.serialization.serializers.ObjectSerializer;
import org.djutils.serialization.serializers.Pointer;
import org.djutils.serialization.serializers.Serializer;
/**
* Decoder for inspection of serialized data.
* <p>
* Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
* </p>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
*/
public class SerialDataDecoder implements Decoder
{
/** The endian util to use to decode multi-byte values. */
private final EndianUtil endianUtil;
/** Type of the data that is currently being decoded. */
private byte currentFieldType;
/** The serializer for the <code>currentFieldType</code>. */
private Serializer<?> currentSerializer = null;
/** Position in the data of the <code>dataElementBytes</code>. */
private int positionInData = -1;
/** Position in the dataElementBytes where the next input byte shall be store. */
private int nextDataElementByte = -1;
/** Collects the bytes that constitute the current data element. */
private byte[] dataElementBytes = new byte[0];
/** Size of the data that is currently being decode. */
private int totalDataSize = -1;
/** Number of rows in an array or matrix. */
private int rowCount;
/** Number of columns in a matrix. */
private int columnCount;
/** Row of matrix or array that we are now reading. */
private int currentRow;
/** Column of matrix that we are now reading. */
private int currentColumn;
/** Djunits display unit. */
private Unit<?> displayUnit;
/** String builder for current output line. */
private StringBuilder buffer = new StringBuilder();
/**
* Construct a new SerialDataDecoder.
* @param endianUtil EndianUtil; the endian util to use to decode multi-byte values
*/
public SerialDataDecoder(final EndianUtil endianUtil)
{
this.endianUtil = endianUtil;
}
@Override
public final String getResult()
{
String result = this.buffer.toString();
this.buffer.setLength(0);
return result;
}
@Override
public final int getMaximumWidth()
{
return 80;
}
@Override
public final boolean append(final int address, final byte theByte) throws IOException
{
boolean result = false;
if (null == this.currentSerializer)
{
// We are expecting a field type byte
this.currentFieldType = theByte;
this.currentSerializer = TypedMessage.PRIMITIVE_DATA_DECODERS.get(this.currentFieldType);
if (null == this.currentSerializer)
{
this.buffer.append(String.format("Bad field type %02x - resynchronizing", this.currentFieldType));
result = true;
// May eventually re-synchronize, but that could take a lot of data.
}
else
{
this.buffer.append(this.currentSerializer.dataClassName() + (this.currentSerializer.getNumberOfDimensions() > 0
|| this.currentSerializer.dataClassName().startsWith("Djunits") ? " " : ": "));
this.positionInData = 1;
this.totalDataSize = 1; // to be adjusted
this.columnCount = 0;
this.rowCount = 0;
this.displayUnit = null;
if (this.currentSerializer.dataClassName().startsWith("String_"))
{
prepareForDataElement(4);
this.totalDataSize += 4;
}
else if (this.currentSerializer.dataClassName().contentEquals("Djunits_vector_array"))
{
prepareForDataElement(8);
this.totalDataSize += 8;
}
else if (this.currentSerializer.getNumberOfDimensions() > 0)
{
int size = this.currentSerializer.getNumberOfDimensions() * 4;
prepareForDataElement(size);
this.totalDataSize += size;
}
else if (this.currentSerializer instanceof ObjectSerializer)
{
try
{
int size;
if (this.currentSerializer.dataClassName().startsWith("Djunits"))
{
// We won't get away calling the size method with null here
// Prepare to get the display unit; requires at least two more bytes
size = 2;
this.displayUnit = null;
}
else
{
size = this.currentSerializer.size(null);
}
prepareForDataElement(size);
this.totalDataSize += size;
}
catch (SerializationException e)
{
e.printStackTrace();
}
}
else
{
try
{
int size = this.currentSerializer.size(null);
this.totalDataSize += size;
prepareForDataElement(size);
}
catch (SerializationException e)
{
e.printStackTrace(); // Cannot happen
}
}
}
return result;
}
if (this.nextDataElementByte < this.dataElementBytes.length)
{
this.dataElementBytes[this.nextDataElementByte] = theByte;
}
this.nextDataElementByte++;
this.positionInData++;
if (this.nextDataElementByte == this.dataElementBytes.length)
{
if (this.currentSerializer.dataClassName().startsWith("String_"))
{
int elementSize = this.currentSerializer.dataClassName().endsWith("8") ? 1 : 2;
if (this.columnCount == 0) // re-using columnCount to store number of characters
{
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
prepareForDataElement(elementSize);
this.totalDataSize += this.columnCount * elementSize;
}
else
{
if (1 == elementSize)
{
if (this.dataElementBytes[0] > 32 && this.dataElementBytes[0] < 127)
{
this.buffer.append((char) this.dataElementBytes[0]); // safe to print
}
else
{
this.buffer.append("."); // not safe to print
}
}
else
{
char character = this.endianUtil.decodeChar(this.dataElementBytes, 0);
if (Character.isAlphabetic(character))
{
this.buffer.append(character); // safe to print
}
else
{
this.buffer.append("."); // not safe to print
}
}
}
this.currentColumn = 0;
this.nextDataElementByte = 0;
}
else if (this.currentSerializer.dataClassName().contentEquals("Djunits_vector_array"))
{
if (this.rowCount == 0)
{
this.rowCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 4);
this.currentRow = -1; // indicates we are parsing the units
this.currentColumn = 0;
prepareForDataElement(2);
this.totalDataSize += 2;
}
else if (this.currentRow < 0)
{
// parse one unit
TypedMessage.getUnit(this.dataElementBytes, new Pointer(), this.endianUtil);
this.displayUnit = TypedMessage.getUnit(this.dataElementBytes, new Pointer(), this.endianUtil);
this.buffer.append("unit for column " + this.currentColumn + ": ");
this.buffer.append(this.displayUnit);
this.currentColumn++;
if (this.currentColumn < this.columnCount)
{
prepareForDataElement(2);
this.totalDataSize += 2;
this.buffer.append(", ");
}
else
{
// Done with the units; prepare to parse the values
this.currentRow = 0;
this.currentColumn = 0;
prepareForDataElement(8);
this.totalDataSize += 8 * this.columnCount * this.rowCount;
}
}
else
{
// process one double value
this.buffer.append(String.format("value at row %d column %d: ", this.currentRow, this.currentColumn));
this.buffer.append(this.endianUtil.decodeDouble(this.dataElementBytes, 0));
this.positionInData = 0;
this.currentColumn++;
if (this.currentColumn >= this.columnCount)
{
this.currentColumn = 0;
this.currentRow++;
}
this.buffer.append(" ");
this.nextDataElementByte = 0;
}
}
else if (this.currentSerializer.dataClassName().startsWith("Djunits"))
{
if (this.currentSerializer.getNumberOfDimensions() > 0 && 0 == this.rowCount)
{
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
this.currentRow = 0;
this.currentColumn = 0;
if (this.dataElementBytes.length == 8)
{
this.rowCount = this.columnCount;
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 4);
this.buffer.append(String.format("height %d, width %d", this.rowCount, this.columnCount));
}
else
{
this.rowCount = 1;
this.buffer.append(String.format("length %d", this.columnCount));
}
// Prepare for the unit.
prepareForDataElement(2);
this.totalDataSize += 2;
this.buffer.append(", ");
return false;
}
else if (null == this.displayUnit)
{
this.displayUnit = TypedMessage.getUnit(this.dataElementBytes, new Pointer(), this.endianUtil);
this.buffer.append("unit " + this.displayUnit);
int numberOfDimensions = this.currentSerializer.getNumberOfDimensions();
int elementSize = this.currentSerializer.dataClassName().contains("Float") ? 4 : 8;
this.totalDataSize += elementSize * (0 == numberOfDimensions ? 1 : this.rowCount * this.columnCount);
prepareForDataElement(elementSize);
if (0 == numberOfDimensions)
{
this.buffer.append(": ");
}
else
{
result = true;
}
}
else
{
// get one value
int dimensions = this.currentSerializer.getNumberOfDimensions();
if (dimensions == 1)
{
this.buffer.append(String.format("value at index %d: ", this.currentColumn));
}
else if (dimensions == 2)
{
this.buffer.append(String.format("value at row %d column %d: ", this.currentRow, this.currentColumn));
}
// else dimension == 0
if (dimensions > 0)
{
this.currentColumn++;
if (this.currentColumn >= this.columnCount)
{
this.currentColumn = 0;
this.currentRow++;
}
}
this.buffer.append(this.dataElementBytes.length == 4 ? this.endianUtil.decodeFloat(this.dataElementBytes, 0)
: this.endianUtil.decodeDouble(this.dataElementBytes, 0));
this.nextDataElementByte = 0;
result = true;
}
}
else if (this.currentSerializer instanceof FixedSizeObjectSerializer)
{
try
{
Object value = this.currentSerializer.deSerialize(this.dataElementBytes, new Pointer(), this.endianUtil);
this.buffer.append(value.toString());
}
catch (SerializationException e)
{
this.buffer.append("Error deserializing data");
}
}
else if (this.currentSerializer.getNumberOfDimensions() > 0)
{
if (this.rowCount == 0)
{
// Got the height and width of a matrix, or length of an array
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 0);
this.currentRow = 0;
this.currentColumn = 0;
if (this.dataElementBytes.length == 8)
{
this.rowCount = this.columnCount;
this.columnCount = this.endianUtil.decodeInt(this.dataElementBytes, 4);
this.buffer.append(String.format("height %d, width %d", this.rowCount, this.columnCount));
}
else
{
this.rowCount = 1;
this.buffer.append(String.format("length %d", this.columnCount));
}
int elementSize = -1;
if (this.currentSerializer instanceof ArrayOrMatrixSerializer<?, ?>)
{
elementSize = ((ArrayOrMatrixSerializer<?, ?>) this.currentSerializer).getElementSize();
}
else if (this.currentSerializer instanceof BasicPrimitiveArrayOrMatrixSerializer)
{
elementSize = ((BasicPrimitiveArrayOrMatrixSerializer<?>) this.currentSerializer).getElementSize();
}
else
{
throw new RuntimeException("Unhandled type of array or matrix serializer");
}
this.totalDataSize += elementSize * this.rowCount * this.columnCount;
prepareForDataElement(elementSize);
// System.out.println("Selecting element size " + elementSize + " for serializer "
// + this.currentSerializer.dataClassName());
}
else
{
// Got one data element
if (this.currentSerializer.getNumberOfDimensions() == 1)
{
this.buffer.append(String.format("value at index %d: ", this.currentColumn));
}
else // 2 dimensions
{
this.buffer.append(String.format("value at row %d column %d: ", this.currentRow, this.currentColumn));
}
if (this.currentSerializer instanceof ArrayOrMatrixSerializer<?, ?>)
{
Object value = ((ArrayOrMatrixSerializer<?, ?>) this.currentSerializer)
.deSerializeElement(this.dataElementBytes, 0, this.endianUtil);
this.buffer.append(value.toString());
}
else if (this.currentSerializer instanceof BasicPrimitiveArrayOrMatrixSerializer)
{
// It looks like we'll have to do this ourselves.
BasicPrimitiveArrayOrMatrixSerializer<?> basicPrimitiveArraySerializer =
(BasicPrimitiveArrayOrMatrixSerializer<?>) this.currentSerializer;
switch (basicPrimitiveArraySerializer.fieldType())
{
case FieldTypes.BYTE_8_ARRAY:
case FieldTypes.BYTE_8_MATRIX:
this.buffer.append(String.format("%02x", this.dataElementBytes[0]));
break;
case FieldTypes.SHORT_16_ARRAY:
case FieldTypes.SHORT_16_MATRIX:
this.buffer.append(String.format("%d", this.endianUtil.decodeShort(this.dataElementBytes, 0)));
break;
case FieldTypes.INT_32_ARRAY:
case FieldTypes.INT_32_MATRIX:
this.buffer.append(String.format("%d", this.endianUtil.decodeInt(this.dataElementBytes, 0)));
break;
case FieldTypes.LONG_64_ARRAY:
case FieldTypes.LONG_64_MATRIX:
this.buffer.append(String.format("%d", this.endianUtil.decodeLong(this.dataElementBytes, 0)));
break;
case FieldTypes.FLOAT_32_ARRAY:
case FieldTypes.FLOAT_32_MATRIX:
this.buffer.append(String.format("%f", this.endianUtil.decodeFloat(this.dataElementBytes, 0)));
break;
case FieldTypes.DOUBLE_64_ARRAY:
case FieldTypes.DOUBLE_64_MATRIX:
this.buffer.append(String.format("%f", this.endianUtil.decodeDouble(this.dataElementBytes, 0)));
break;
case FieldTypes.BOOLEAN_8_ARRAY:
case FieldTypes.BOOLEAN_8_MATRIX:
this.buffer.append(0 == this.dataElementBytes[0] ? "false" : "true");
break;
default:
throw new RuntimeException(
"Unhandled type of basicPrimitiveArraySerializer: " + basicPrimitiveArraySerializer);
}
}
this.nextDataElementByte = 0;
this.currentColumn++;
if (this.currentColumn == this.columnCount)
{
this.currentColumn = 0;
this.currentRow++;
}
}
// System.out.println(
// "Parsed 1 element; next element is for column " + this.currentColumn + ", row " + this.currentRow);
result = true;
}
}
if (this.positionInData == this.totalDataSize)
{
this.currentSerializer = null;
this.positionInData = -1;
this.totalDataSize = -1;
this.rowCount = 0;
this.columnCount = 0;
return true;
}
return result;
}
/**
* Allocate a buffer for the next data element (or two).
* @param dataElementSize int; size of the buffer
*/
private void prepareForDataElement(final int dataElementSize)
{
this.dataElementBytes = new byte[dataElementSize];
this.nextDataElementByte = 0;
}
@Override
public final boolean ignoreForIdenticalOutputCheck()
{
return false;
}
}