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