Bug 560477: state system: Serialize unknown state types as strings

If a state object is not one of the known object types (Null, Long, Int,
Double, String, or CustomStateValue) then an IllegalArgumentException
was thrown. For example, when using an XML analysis with a CTF trace
which has an event field of type CtfEnumPair and this CtfEnumPair is
used as state value of an attribute, then such an exception would be
created.

This patch will convert the state value to a string, if the value object
is not one of the known types.

[Fixed] Bug 560477: IllegalArgumentException in SS serialization

Change-Id: I96f3b7cc3df7113579531251a98235a219a4d0c0
Signed-off-by: Bernd Hufmann <Bernd.Hufmann@ericsson.com>
Reviewed-on: https://git.eclipse.org/r/158248
Tested-by: Trace Compass Bot <tracecompass-bot@eclipse.org>
Reviewed-by: Genevieve Bastien <gbastien+lttng@versatic.net>
Tested-by: Genevieve Bastien <gbastien+lttng@versatic.net>
diff --git a/statesystem/org.eclipse.tracecompass.statesystem.core.tests/src/org/eclipse/tracecompass/statesystem/core/tests/backend/historytree/HTIntervalObjectReadWriteTest.java b/statesystem/org.eclipse.tracecompass.statesystem.core.tests/src/org/eclipse/tracecompass/statesystem/core/tests/backend/historytree/HTIntervalObjectReadWriteTest.java
new file mode 100644
index 0000000..a41b478
--- /dev/null
+++ b/statesystem/org.eclipse.tracecompass.statesystem.core.tests/src/org/eclipse/tracecompass/statesystem/core/tests/backend/historytree/HTIntervalObjectReadWriteTest.java
@@ -0,0 +1,136 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Ericsson
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.statesystem.core.tests.backend.historytree;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.tracecompass.internal.statesystem.core.backend.historytree.HTInterval;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Test the reading/writing logic in {@link HTInterval}, including unknown
+ * object types.
+ *
+ * @author Bernd Hufmann
+ */
+@RunWith(Parameterized.class)
+@NonNullByDefault({})
+public class HTIntervalObjectReadWriteTest {
+    private Object fTestObject;
+
+    /**
+     * Parameter generator.
+     *
+     * Generates different objects to be serialized.
+     *
+     * @return The test parameters
+     */
+    /**
+     * @return The arrays of parameters
+     */
+    @Parameters(name = "{index}: {0}")
+    public static Iterable<Object[]> getParameters() {
+        List<Object[]> list = new ArrayList<>();
+        Object array[] = new Object[] { String.valueOf("Hello") };
+        list.add(array);
+        array = new Object[] { new TestObject("Test", 0) };
+        list.add(array);
+        return list;
+    }
+
+    /**
+     * Test constructor
+     *
+     * @param obj
+     *            The object to put in state system.
+     */
+    public HTIntervalObjectReadWriteTest(Object obj) {
+        fTestObject = obj;
+    }
+
+    /**
+     * Test method.
+     *
+     * @throws IOException
+     *             Fails the test
+     */
+    @Test
+    public void testHtInterval() throws IOException {
+        HTInterval interval = new HTInterval(0, 10, 1, fTestObject);
+        writeAndReadInterval(interval);
+    }
+
+    private static void writeAndReadInterval(HTInterval interval) throws IOException {
+        int sizeOnDisk = interval.getSizeOnDisk();
+
+        /* Write the interval to a file */
+        File tempFile = File.createTempFile("test-interval", ".interval");
+        try (FileOutputStream fos = new FileOutputStream(tempFile, false);
+                FileChannel fc = fos.getChannel();) {
+
+            ByteBuffer bb = ByteBuffer.allocate(sizeOnDisk);
+            bb.order(ByteOrder.LITTLE_ENDIAN);
+            bb.clear();
+
+            interval.writeInterval(bb, 1);
+            bb.flip();
+            int written = fc.write(bb);
+            assertEquals(sizeOnDisk, written);
+        }
+
+        /* Read the interval from the file */
+        HTInterval readInterval;
+        try (FileInputStream fis = new FileInputStream(tempFile);
+                FileChannel fc = fis.getChannel();) {
+
+            ByteBuffer bb = ByteBuffer.allocate(sizeOnDisk);
+            bb.order(ByteOrder.LITTLE_ENDIAN);
+            bb.clear();
+
+            int read = fc.read(bb);
+            assertEquals(sizeOnDisk, read);
+            bb.flip();
+            readInterval = HTInterval.readFrom(bb, 1);
+        }
+
+        assertEquals(interval.toString(), readInterval.toString());
+    }
+
+    private static class TestObject {
+        private final String fName;
+        private final int fId;
+        private TestObject(String name, int id) {
+            fName = name;
+            fId = id;
+        }
+        @Override
+        public String toString() {
+            StringBuilder b = new StringBuilder();
+            b.append(fName).append(fId);
+            return b.toString();
+        }
+    }
+}
diff --git a/statesystem/org.eclipse.tracecompass.statesystem.core/src/org/eclipse/tracecompass/internal/statesystem/core/backend/historytree/HTInterval.java b/statesystem/org.eclipse.tracecompass.statesystem.core/src/org/eclipse/tracecompass/internal/statesystem/core/backend/historytree/HTInterval.java
index 6297cdd..93bb194 100644
--- a/statesystem/org.eclipse.tracecompass.statesystem.core/src/org/eclipse/tracecompass/internal/statesystem/core/backend/historytree/HTInterval.java
+++ b/statesystem/org.eclipse.tracecompass.statesystem.core/src/org/eclipse/tracecompass/internal/statesystem/core/backend/historytree/HTInterval.java
@@ -65,6 +65,9 @@
     /**
      * Standard constructor
      *
+     * Note: Unknown object type of the state value will be serialized
+     * and deserialized as String.
+     *
      * @param intervalStart
      *            Start time of the interval
      * @param intervalEnd
@@ -116,27 +119,21 @@
             return (minSize + Long.BYTES);
         } else if (stateValue instanceof Double) {
             return (minSize + Double.BYTES);
-        } else if (stateValue instanceof String) {
-            String str = (String) stateValue;
-            int strLength = str.getBytes(CHARSET).length;
-
-            if (strLength > Short.MAX_VALUE) {
-                throw new IllegalArgumentException("String is too long to be stored in state system: " + str); //$NON-NLS-1$
-            }
-
-            /*
-             * String's length + 3 (2 bytes for size, 1 byte for \0 at the end)
-             */
-            return (minSize + strLength + 3);
         } else if (stateValue instanceof CustomStateValue) {
             /* Length of serialized value (short) + state value */
             return (minSize + Short.BYTES + ((CustomStateValue) stateValue).getSerializedSize());
         }
+        String str = String.valueOf(stateValue);
+        int strLength = str.getBytes(CHARSET).length;
+
+        if (strLength > Short.MAX_VALUE) {
+            throw new IllegalArgumentException("String is too long to be stored in state system: " + str); //$NON-NLS-1$
+        }
+
         /*
-         * It's very important that we know how to write the state value in the
-         * file!!
+         * String's length + 3 (2 bytes for size, 1 byte for \0 at the end)
          */
-        throw new IllegalStateException();
+        return (minSize + strLength + 3);
     }
 
     /**
@@ -281,9 +278,15 @@
             } else if (value instanceof Double) {
                 buffer.put(TYPE_DOUBLE);
                 buffer.putDouble((double) value);
-            } else if (value instanceof String) {
+            } else if (value instanceof CustomStateValue) {
+                buffer.put(TYPE_CUSTOM);
+                int size = ((CustomStateValue) value).getSerializedSize();
+                buffer.putShort((short) size);
+                ISafeByteBufferWriter safeBuffer = SafeByteBufferFactory.wrapWriter(buffer, size);
+                ((CustomStateValue) value).serialize(safeBuffer);
+            } else {
+                String string = String.valueOf(value);
                 buffer.put(TYPE_STRING);
-                String string = (String) value;
                 byte[] strArray = string.getBytes(CHARSET);
 
                 /*
@@ -293,14 +296,6 @@
                 buffer.putShort((short) strArray.length);
                 buffer.put(strArray);
                 buffer.put((byte) 0);
-            } else if (value instanceof CustomStateValue) {
-                buffer.put(TYPE_CUSTOM);
-                int size = ((CustomStateValue) value).getSerializedSize();
-                buffer.putShort((short) size);
-                ISafeByteBufferWriter safeBuffer = SafeByteBufferFactory.wrapWriter(buffer, size);
-                ((CustomStateValue) value).serialize(safeBuffer);
-            } else {
-                throw new IllegalStateException("Type: " + value.getClass() + " is not implemented in the state system"); //$NON-NLS-1$ //$NON-NLS-2$
             }
         } else {
             buffer.put(TYPE_NULL);