View Javadoc
1   package org.djutils.serialization;
2   
3   import java.io.IOException;
4   
5   import org.djunits.unit.Unit;
6   import org.djunits.value.vdouble.scalar.SIScalar;
7   import org.djunits.value.vdouble.scalar.base.DoubleScalar;
8   import org.djunits.value.vfloat.scalar.FloatSIScalar;
9   import org.djunits.value.vfloat.scalar.base.FloatScalar;
10  import org.djutils.decoderdumper.Decoder;
11  import org.djutils.logger.CategoryLogger;
12  import org.djutils.serialization.serializers.ArrayOrMatrixWithUnitSerializer;
13  import org.djutils.serialization.serializers.BasicPrimitiveArrayOrMatrixSerializer;
14  import org.djutils.serialization.serializers.FixedSizeObjectSerializer;
15  import org.djutils.serialization.serializers.Pointer;
16  import org.djutils.serialization.serializers.Serializer;
17  import org.djutils.serialization.serializers.StringArraySerializer;
18  import org.djutils.serialization.serializers.StringMatrixSerializer;
19  
20  /**
21   * Decoder for inspection of serialized data. The SerialDataDecoder implements a state machine that processes one byte at a
22   * time. Output is sent to the buffer (a StringBuilder).
23   * <p>
24   * Copyright (c) 2013-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
25   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
26   * </p>
27   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
28   * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
29   */
30  public class SerialDataDecoder implements Decoder
31  {
32      /** The endian util to use to decode multi-byte values. */
33      private final Endianness endianness;
34  
35      /** Type of the data that is currently being decoded. */
36      private byte currentFieldType;
37  
38      /** The serializer for the <code>currentFieldType</code>. */
39      private Serializer<?> currentSerializer = null;
40  
41      /** Position in the dataElementBytes where the next input byte shall be store. */
42      private int nextDataElementByte = -1;
43  
44      /** Collects the bytes that constitute the current data element. */
45      private byte[] dataElementBytes = new byte[0];
46  
47      /** Number of rows in an array or matrix. */
48      private int rowCount;
49  
50      /** Number of columns in a matrix. */
51      private int columnCount;
52  
53      /** Number of characters in a string. */
54      private int charCount;
55  
56      /** Row of matrix or array that we are now reading. */
57      private int currentRow;
58  
59      /** Column of matrix that we are now reading. */
60      private int currentColumn;
61  
62      /** Character in the string that we are currently reading (either 8 or 16 bits). */
63      private int currentChar;
64  
65      /** Djunits display unit. */
66      private Unit<?> displayUnit;
67  
68      /** Array of units for array of column vectors. */
69      private Unit<?>[] columnUnits = null;
70  
71      /** String builder for current output line. */
72      private StringBuilder buffer = new StringBuilder();
73  
74      /**
75       * Construct a new SerialDataDecoder.
76       * @param endianness the endian util to use to decode multi-byte values
77       */
78      public SerialDataDecoder(final Endianness endianness)
79      {
80          this.endianness = endianness;
81      }
82  
83      @Override
84      public final String getResult()
85      {
86          String result = this.buffer.toString();
87          this.buffer.setLength(0);
88          return result;
89      }
90  
91      @Override
92      public final int getMaximumWidth()
93      {
94          return 80;
95      }
96  
97      /**
98       * Decode one (more) byte. This method must return true when a line becomes full due to this call, otherwise this method
99       * must return false.
100      * @param address the address that corresponds with the byte, for printing purposes.
101      * @param theByte the byte to process
102      * @return true if an output line has been completed by this call; false if at least one more byte can be appended to the
103      *         local accumulator before the current output line is full
104      * @throws IOException when the output device throws this exception
105      */
106     @Override
107     public final boolean append(final int address, final byte theByte) throws IOException
108     {
109         boolean result = false;
110 
111         // check if first byte to indicate the field type
112         if (this.currentSerializer == null)
113         {
114             result = processFieldTypeByte(theByte);
115             return result;
116         }
117 
118         // add byte to data element
119         if (this.nextDataElementByte < this.dataElementBytes.length)
120         {
121             this.dataElementBytes[this.nextDataElementByte] = theByte;
122         }
123         this.nextDataElementByte++;
124         // if data element complete, process it, and prepare for next data element (if any in current field type)
125         if (this.nextDataElementByte == this.dataElementBytes.length)
126         {
127             result = processDataElement();
128         }
129 
130         // are we done?
131         if (this.currentSerializer == null)
132         {
133             return true;
134         }
135         return result;
136     }
137 
138     /**
139      * Process one byte that indicates the field type.
140      * @param fieldType the byte for the field type to process
141      * @return whether line is full
142      */
143     private boolean processFieldTypeByte(final byte fieldType)
144     {
145         this.currentFieldType = fieldType;
146         this.currentSerializer = TypedObject.PRIMITIVE_DATA_DECODERS.get(this.currentFieldType);
147         if (this.currentSerializer == null)
148         {
149             this.buffer.append(String.format("Error: Bad field type %02x - resynchronizing", this.currentFieldType));
150             return true;
151         }
152         this.buffer.append(this.currentSerializer.dataClassName() + (this.currentSerializer.getNumberOfDimensions() > 0
153                 || this.currentSerializer.dataClassName().startsWith("Djunits") ? " " : ": "));
154 
155         this.columnCount = 0;
156         this.rowCount = 0;
157         this.displayUnit = null;
158         this.columnUnits = null;
159 
160         // check the type and prepare for what is expected; primitive types
161         if (this.currentSerializer instanceof FixedSizeObjectSerializer<?>)
162         {
163             var fsoe = (FixedSizeObjectSerializer<?>) this.currentSerializer;
164             int size = fsoe.size(null);
165             prepareForDataElement(size);
166             return false;
167         }
168 
169         // array or matrix type: next variable to expect is one or more ints (rows/cols)
170         if (this.currentSerializer.getNumberOfDimensions() > 0)
171         {
172             int size = this.currentSerializer.getNumberOfDimensions() * 4;
173             prepareForDataElement(size);
174             return false;
175         }
176 
177         // regular string type, next is an int for length
178         if (this.currentFieldType == 9 || this.currentFieldType == 10)
179         {
180             prepareForDataElement(4);
181             return false;
182         }
183 
184         // djunits scalar type, next is quantity type and unit
185         if (this.currentFieldType == 25 || this.currentFieldType == 26)
186         {
187             prepareForDataElement(2);
188             return false;
189         }
190 
191         this.buffer
192             .append(String.format("Error: No field type handler for type %02x - resynchronizing", this.currentFieldType));
193         return true;
194     }
195 
196     /**
197      * Process a completed data element. If the current data type has more elements, prepare for the next data element.
198      * @return whether the line is full or not
199      */
200     private boolean processDataElement()
201     {
202         boolean result = false;
203 
204         // primitive types
205         if (this.currentSerializer instanceof FixedSizeObjectSerializer<?>)
206         {
207             result = appendFixedSizeObject();
208             done();
209             return result;
210         }
211 
212         // regular string type
213         if (this.currentFieldType == 9 || this.currentFieldType == 10)
214         {
215             appendString();
216             if (this.currentChar >= this.charCount)
217             {
218                 done();
219             }
220             return false;
221         }
222 
223         // processing of vector array type before array or matrix type
224         if (this.currentFieldType == 31 || this.currentFieldType == 32)
225         {
226             if (this.rowCount == 0)
227             {
228                 processRowsCols();
229                 prepareForDataElement(2 * this.columnCount); // unit type and display type for every column
230                 return false;
231             }
232             if (this.columnUnits == null)
233             {
234                 return fillDjunitsVectorArrayColumnUnits();
235             }
236             return appendDjunitsVectorArrayElement();
237         }
238 
239         // array or matrix type
240         if (this.currentSerializer.getNumberOfDimensions() > 0)
241         {
242             if (this.rowCount == 0)
243             {
244                 processRowsCols();
245                 if (this.currentSerializer.hasUnit())
246                 {
247                     prepareForDataElement(2); // unit type and display type
248                 }
249                 else if (this.currentSerializer instanceof BasicPrimitiveArrayOrMatrixSerializer<?>)
250                 {
251                     var bpams = (BasicPrimitiveArrayOrMatrixSerializer<?>) this.currentSerializer;
252                     prepareForDataElement(bpams.getElementSize());
253                 }
254                 else if (this.currentSerializer instanceof StringArraySerializer
255                         || this.currentSerializer instanceof StringMatrixSerializer)
256                 {
257                     prepareForDataElement(4);
258                 }
259                 return false;
260             }
261             if (this.currentSerializer.hasUnit())
262             {
263                 if (this.displayUnit == null)
264                 {
265                     result = processUnit();
266                     prepareForDataElement(((ArrayOrMatrixWithUnitSerializer<?, ?>) this.currentSerializer).getElementSize());
267                     return result;
268                 }
269                 result = appendDjunitsElement();
270                 prepareForDataElement(this.dataElementBytes.length);
271                 incColumnCount();
272                 return result;
273             }
274             if (this.currentSerializer instanceof StringArraySerializer
275                     || this.currentSerializer instanceof StringMatrixSerializer)
276             {
277                 processStringElement();
278                 return false;
279             }
280             result = appendPrimitiveElement();
281             prepareForDataElement(this.dataElementBytes.length);
282             incColumnCount();
283             return result;
284         }
285 
286         // djunits scalar type
287         if (this.currentFieldType == 25 || this.currentFieldType == 26)
288         {
289             if (this.displayUnit == null)
290             {
291                 result = processUnit();
292                 prepareForDataElement(getSize() - 2); // subtract unit bytes
293                 return result;
294             }
295             result = appendDjunitsElement();
296             done();
297             return result;
298         }
299 
300         // any leftovers?
301         CategoryLogger.always().warn("Did not process type {}", this.currentFieldType);
302         return true;
303     }
304 
305     /**
306      * Return the size of the encoding for a fixed size data element.
307      * @return the encoding size of an element
308      */
309     private int getSize()
310     {
311         try
312         {
313             return this.currentSerializer.size(null);
314         }
315         catch (SerializationException e)
316         {
317             CategoryLogger.always().error(e, "Could not determine size of element for field type {}", this.currentFieldType);
318             return 1;
319         }
320     }
321 
322     /**
323      * Append a fixed size object (i.e., primitive type) to the buffer.
324      * @return whether the line is full or not
325      */
326     private boolean appendFixedSizeObject()
327     {
328         try
329         {
330             Object value = this.currentSerializer.deSerialize(this.dataElementBytes, new Pointer(), this.endianness);
331             this.buffer.append(value.toString());
332         }
333         catch (SerializationException e)
334         {
335             this.buffer.append("Error deserializing data");
336             return true;
337         }
338         return false;
339     }
340 
341     /**
342      * Append a character of a string to the buffer.
343      */
344     private void appendString()
345     {
346         int elementSize = this.currentSerializer.dataClassName().contains("8") ? 1 : 2;
347         if (this.charCount == 0)
348         {
349             this.charCount = this.endianness.decodeInt(this.dataElementBytes, 0);
350             this.currentChar = 0;
351             prepareForDataElement(elementSize);
352         }
353         else
354         {
355             if (elementSize == 1)
356             {
357                 if (this.dataElementBytes[0] > 32 && this.dataElementBytes[0] < 127)
358                 {
359                     this.buffer.append((char) this.dataElementBytes[0]); // safe to print
360                 }
361                 else
362                 {
363                     this.buffer.append("."); // not safe to print
364                 }
365             }
366             else
367             {
368                 char character = this.endianness.decodeChar(this.dataElementBytes, 0);
369                 if (Character.isAlphabetic(character))
370                 {
371                     this.buffer.append(character); // safe to print
372                 }
373                 else
374                 {
375                     this.buffer.append("."); // not safe to print
376                 }
377             }
378             this.currentChar++;
379         }
380         this.nextDataElementByte = 0;
381     }
382 
383     /**
384      * Process a string element in a string array or string matrix.
385      */
386     private void processStringElement()
387     {
388         appendString();
389         if (this.currentChar >= this.charCount)
390         {
391             incColumnCount();
392             this.charCount = 0;
393             prepareForDataElement(4);
394         }
395     }
396 
397     /**
398      * Process the height and width of a matrix, or length of an array.
399      */
400     private void processRowsCols()
401     {
402         this.currentRow = 0;
403         this.currentColumn = 0;
404         if (this.dataElementBytes.length == 8)
405         {
406             this.rowCount = this.endianness.decodeInt(this.dataElementBytes, 0);
407             this.columnCount = this.endianness.decodeInt(this.dataElementBytes, 4);
408             this.buffer.append(String.format("height %d, width %d: ", this.rowCount, this.columnCount));
409         }
410         else
411         {
412             this.columnCount = this.endianness.decodeInt(this.dataElementBytes, 0);
413             this.rowCount = 1;
414             this.buffer.append(String.format("length %d: ", this.columnCount));
415         }
416     }
417 
418     /**
419      * Fill the unit type for the columns of this special type.
420      * @return whether line is full or not.
421      */
422     private boolean fillDjunitsVectorArrayColumnUnits()
423     {
424         boolean result = false;
425         this.columnUnits = new Unit<?>[this.columnCount];
426         for (int i = 0; i < this.columnCount; i++)
427         {
428             byte unitTypeCode = this.dataElementBytes[2 * i];
429             byte displayUnitCode = this.dataElementBytes[2 * i + 1];
430             this.columnUnits[i] = UnitType.getUnit(unitTypeCode, displayUnitCode);
431             if (this.columnUnits[i] == null && !result)
432             {
433                 this.buffer.append(
434                         String.format("Error: Could not find unit type %d, display unit %d", unitTypeCode, displayUnitCode));
435                 result = true;
436             }
437         }
438         prepareForDataElement(this.currentFieldType == 31 ? 4 : 8);
439         return result;
440     }
441 
442     /**
443      * Append one (row) element in an array of (column) vectors.
444      * @return whether the line is full or not
445      * @param <U> the unit type
446      * @param <FS> the float scalar type
447      * @param <DS> the double scalar type
448      */
449     @SuppressWarnings("unchecked")
450     private <U extends Unit<U>, FS extends FloatScalar<U, FS>,
451             DS extends DoubleScalar<U, DS>> boolean appendDjunitsVectorArrayElement()
452     {
453         boolean result = false;
454         try
455         {
456             U unit = (U) this.columnUnits[this.currentColumn];
457             if (this.currentFieldType == 31)
458             {
459                 float f = this.endianness.decodeFloat(this.dataElementBytes, 0);
460                 FloatScalar<U, FS> afs = FloatSIScalar.instantiateAnonymous(f, unit.getStandardUnit());
461                 afs.setDisplayUnit(unit);
462                 this.buffer.append(afs.toDisplayString().replace(" ", "") + " ");
463             }
464             else
465             {
466                 double d = this.endianness.decodeDouble(this.dataElementBytes, 0);
467                 DoubleScalar<U, DS> ads = SIScalar.instantiateAnonymous(d, unit.getStandardUnit());
468                 ads.setDisplayUnit(unit);
469                 this.buffer.append(ads.toDisplayString().replace(" ", "") + " ");
470             }
471         }
472         catch (Exception e)
473         {
474             this.buffer.append("Error: Illegal element in vector array -- could not parse");
475             result = true;
476         }
477         prepareForDataElement(this.dataElementBytes.length);
478         incColumnCount();
479         return result;
480     }
481 
482     /**
483      * Process a unit (2 bytes).
484      * @return whether the line is full or not
485      */
486     private boolean processUnit()
487     {
488         byte unitTypeCode = this.dataElementBytes[0];
489         byte displayUnitCode = this.dataElementBytes[1];
490         this.displayUnit = UnitType.getUnit(unitTypeCode, displayUnitCode);
491         if (this.displayUnit == null)
492         {
493             this.buffer
494                 .append(String.format("Error: Could not find unit ype %d, display unit %d", unitTypeCode, displayUnitCode));
495             return true;
496         }
497         return false;
498     }
499 
500     /**
501      * Process one element of a djunits vector or array.
502      * @return whether the line is full or not
503      * @param <U> the unit type
504      * @param <FS> the float scalar type
505      * @param <DS> the double scalar type
506      */
507     @SuppressWarnings("unchecked")
508     private <U extends Unit<U>, FS extends FloatScalar<U, FS>, DS extends DoubleScalar<U, DS>> boolean appendDjunitsElement()
509     {
510         boolean result = false;
511         try
512         {
513             if (this.dataElementBytes.length == 4)
514             {
515                 float f = this.endianness.decodeFloat(this.dataElementBytes, 0);
516                 FloatScalar<U, FS> afs = FloatSIScalar.instantiateAnonymous(f, this.displayUnit.getStandardUnit());
517                 afs.setDisplayUnit((U) this.displayUnit);
518                 this.buffer.append(afs.toDisplayString().replace(" ", "") + " ");
519             }
520             else
521             {
522                 double d = this.endianness.decodeDouble(this.dataElementBytes, 0);
523                 DoubleScalar<U, DS> ads = SIScalar.instantiateAnonymous(d, this.displayUnit.getStandardUnit());
524                 ads.setDisplayUnit((U) this.displayUnit);
525                 this.buffer.append(ads.toDisplayString().replace(" ", "") + " ");
526             }
527         }
528         catch (Exception e)
529         {
530             this.buffer.append("Error: Could not instantiate djunits element");
531             result = true;
532         }
533         return result;
534     }
535 
536     /**
537      * Append a primitive element to the buffer.
538      * @return whether the line is full or not.
539      */
540     private boolean appendPrimitiveElement()
541     {
542         boolean result = false;
543         this.buffer.append(switch (this.currentSerializer.fieldType())
544         {
545             // @formatter:off
546             case FieldTypes.BYTE_8_ARRAY, FieldTypes.BYTE_8_MATRIX -> 
547                 String.format("%02x ", this.dataElementBytes[0]);
548             case FieldTypes.SHORT_16_ARRAY, FieldTypes.SHORT_16_MATRIX -> 
549                 String.format("%d ", this.endianness.decodeShort(this.dataElementBytes, 0));
550             case FieldTypes.INT_32_ARRAY, FieldTypes.INT_32_MATRIX -> 
551                 String.format("%d ", this.endianness.decodeInt(this.dataElementBytes, 0));
552             case FieldTypes.LONG_64_ARRAY, FieldTypes.LONG_64_MATRIX -> 
553                 String.format("%d ", this.endianness.decodeLong(this.dataElementBytes, 0));
554             case FieldTypes.FLOAT_32_ARRAY, FieldTypes.FLOAT_32_MATRIX -> 
555                 String.format("%f ", this.endianness.decodeFloat(this.dataElementBytes, 0));
556             case FieldTypes.DOUBLE_64_ARRAY, FieldTypes.DOUBLE_64_MATRIX -> 
557                 String.format("%f ", this.endianness.decodeDouble(this.dataElementBytes, 0));
558             case FieldTypes.BOOLEAN_8_ARRAY, FieldTypes.BOOLEAN_8_MATRIX -> 
559                 this.dataElementBytes[0] == 0 ? "false " : "true ";
560             // @formatter:on
561             default -> "Error: Unhandled type of basicPrimitiveArraySerializer: " + this.currentSerializer.fieldType();
562         });
563         return result;
564     }
565 
566     /**
567      * Increase the column count and possibly row count. Reset when complete.
568      */
569     private void incColumnCount()
570     {
571         this.currentColumn++;
572         if (this.currentColumn >= this.columnCount)
573         {
574             this.currentColumn = 0;
575             this.currentRow++;
576             if (this.currentRow >= this.rowCount)
577             {
578                 done();
579             }
580         }
581     }
582 
583     /**
584      * Reset the state when done with the current variable.
585      */
586     private void done()
587     {
588         this.currentSerializer = null;
589         this.rowCount = 0;
590         this.columnCount = 0;
591         this.charCount = 0;
592     }
593 
594     /**
595      * Allocate a buffer for the next data element (or two).
596      * @param dataElementSize size of the buffer
597      */
598     private void prepareForDataElement(final int dataElementSize)
599     {
600         this.dataElementBytes = new byte[dataElementSize];
601         this.nextDataElementByte = 0;
602     }
603 
604     @Override
605     public final boolean ignoreForIdenticalOutputCheck()
606     {
607         return false;
608     }
609 
610 }