diff --git a/engine/api/src/main/java/io/deephaven/engine/table/ColumnSource.java b/engine/api/src/main/java/io/deephaven/engine/table/ColumnSource.java index c70c2fe65a0..2e469486904 100644 --- a/engine/api/src/main/java/io/deephaven/engine/table/ColumnSource.java +++ b/engine/api/src/main/java/io/deephaven/engine/table/ColumnSource.java @@ -177,8 +177,34 @@ default void exportAllTo(final Object @NotNull [] dest, @NotNull final T tuple) */ @FinalDefault default ColumnSource cast(Class clazz) { + return cast(clazz, (String) null); + } + + /** + * Returns this {@code ColumnSource}, parameterized by {@code }, if the data type of this column (as given by + * {@link #getType()}) can be cast to {@code clazz}. This is analogous to casting the objects provided by this + * column source to {@code clazz}. + *

+ * For example, the following code will throw an exception if the "MyString" column does not actually contain + * {@code String} data: + * + *

+     *     ColumnSource<String> colSource = table.getColumnSource("MyString").cast(String.class, "MyString")
+     * 
+ *

+ * Due to the nature of type erasure, the JVM will still insert an additional cast to {@code TYPE} when elements are + * retrieved from the column source, such as with {@code String myStr = colSource.get(0)}. + * + * @param clazz The target type. + * @param The target type, as a type parameter. Intended to be inferred from {@code clazz}. + * @param colName An optional column name, which will be included in exception messages. + * @return A {@code ColumnSource} parameterized by {@code TYPE}. + */ + @FinalDefault + default ColumnSource cast(Class clazz, @Nullable String colName) { Require.neqNull(clazz, "clazz"); - TypeHelper.checkCastTo("ColumnSource", getType(), clazz); + final String castCheckPrefix = colName == null ? "ColumnSource" : "ColumnSource[" + colName + ']'; + TypeHelper.checkCastTo(castCheckPrefix, getType(), clazz); // noinspection unchecked return (ColumnSource) this; } @@ -208,8 +234,39 @@ default ColumnSource cast(Class clazz) { */ @FinalDefault default ColumnSource cast(Class clazz, @Nullable Class componentType) { + return cast(clazz, componentType, null); + } + + /** + * Returns this {@code ColumnSource}, parameterized by {@code }, if the data type of this column (as given by + * {@link #getType()}) can be cast to {@code clazz}. This is analogous to casting the objects provided by this + * column source to {@code clazz}. Additionally, this checks that the component type of this column (as given by + * {@link #getComponentType()}) can be cast to {@code componentType} (both must be present and castable, or both + * must be {@code null}). + * + *

+ * For example, the following code will throw an exception if the "MyString" column does not actually contain + * {@code String} data: + * + *

+     *     ColumnSource<String> colSource = table.getColumnSource("MyString").cast(String.class, null, "MyString")
+     * 
+ *

+ * Due to the nature of type erasure, the JVM will still insert an additional cast to {@code TYPE} when elements are + * retrieved from the column source, such as with {@code String myStr = colSource.get(0)}. + * + * @param clazz The target type. + * @param componentType The target component type, may be {@code null}. + * @param colName An optional column name, which will be included in exception messages. + * @param The target type, as a type parameter. Intended to be inferred from {@code clazz}. + * @return A {@code ColumnSource} parameterized by {@code TYPE}. + */ + @FinalDefault + default ColumnSource cast(Class clazz, @Nullable Class componentType, + @Nullable String colName) { Require.neqNull(clazz, "clazz"); - TypeHelper.checkCastTo("ColumnSource", getType(), getComponentType(), clazz, componentType); + final String castCheckPrefix = colName == null ? "ColumnSource" : "ColumnSource[" + colName + ']'; + TypeHelper.checkCastTo(castCheckPrefix, getType(), getComponentType(), clazz, componentType); // noinspection unchecked return (ColumnSource) this; } diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/TableDefaults.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/TableDefaults.java index 9600e9021d0..d815bc5bd22 100644 --- a/engine/table/src/main/java/io/deephaven/engine/table/impl/TableDefaults.java +++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/TableDefaults.java @@ -99,7 +99,7 @@ default ColumnSource getColumnSource(String sourceName, Class ColumnSource getColumnSource(String sourceName, Class merge(s1, s2, r2).getUpdateGraph())); } + public void testColumnSourceCast() { + // Create a test table with a String column and an array column + final Table testTable = TableTools.newTable( + TableTools.stringCol("MyTestStrCol", "A", "B", "C"), + TableTools.col("MyTestArrCol", new String[] {"A0", "A1"}, new String[] {"B0", "B1"}, + new String[] {"C0", "C1"})); + + /* Test getting column sources with checked types */ + + // Test getColumnSource for MyTestStrCol + ColumnSource stringColSource = testTable.getColumnSource("MyTestStrCol", CharSequence.class); + assertNotNull(stringColSource); + assertEquals(String.class, stringColSource.getType()); // actual type is still String + + // Test getColumnSource for MyTestStrCol with wrong type and verify exception message + ClassCastException colTypeException = Assert.assertThrows(ClassCastException.class, () -> { + ColumnSource intColSource = testTable.getColumnSource("MyTestStrCol", Integer.class); + }); + assertEquals("Cannot convert ColumnSource[MyTestStrCol] of type java.lang.String to type java.lang.Integer", + colTypeException.getMessage()); + + // Test getColumnSource for MyTestArrCol + ColumnSource arrColSource = + testTable.getColumnSource("MyTestArrCol", CharSequence[].class, CharSequence.class); + assertNotNull(arrColSource); + assertEquals(String[].class, arrColSource.getType()); + assertEquals(String.class, arrColSource.getComponentType()); + + // Test getColumnSource for MyTestArrCol with a wrong component type and verify exception message + ClassCastException wrongComponentException = Assert.assertThrows(ClassCastException.class, () -> { + ColumnSource wrongComponentTypeSource = + testTable.getColumnSource("MyTestArrCol", CharSequence[].class, Integer.class); + }); + assertEquals( + "Cannot convert ColumnSource[MyTestArrCol] componentType of type java.lang.String to java.lang.Integer (for [Ljava.lang.String; / [Ljava.lang.CharSequence;)", + wrongComponentException.getMessage()); + + + /* Verify exception messages of underlying ColumnSource.cast method, with and without column name specified */ + + ColumnSource rawStrColSource = testTable.getColumnSource("MyTestStrCol"); + // cast() without component type, with column name specified + ClassCastException castExceptionNoCompWithColName = Assert.assertThrows(ClassCastException.class, () -> { + rawStrColSource.cast(Boolean.class, "MyTestStrCol"); + }); + assertEquals("Cannot convert ColumnSource[MyTestStrCol] of type java.lang.String to type java.lang.Boolean", + castExceptionNoCompWithColName.getMessage()); + + // cast() without component type and no column name specified + ClassCastException castExceptionNoCompNoColName = Assert.assertThrows(ClassCastException.class, () -> { + rawStrColSource.cast(Boolean.class); + }); + assertEquals("Cannot convert ColumnSource of type java.lang.String to type java.lang.Boolean", + castExceptionNoCompNoColName.getMessage()); + + ColumnSource rawArrColSource = testTable.getColumnSource("MyTestArrCol"); + // cast() with component type and column name specified + ClassCastException castExceptionWithCompAndColName = Assert.assertThrows(ClassCastException.class, () -> { + rawArrColSource.cast(Object[].class, Integer.class, "MyTestArrCol"); + }); + assertEquals( + "Cannot convert ColumnSource[MyTestArrCol] componentType of type java.lang.String to java.lang.Integer (for [Ljava.lang.String; / [Ljava.lang.Object;)", + castExceptionWithCompAndColName.getMessage()); + + // cast() with component type and no column name specified + ClassCastException castExceptionWithCompNoColName = Assert.assertThrows(ClassCastException.class, () -> { + rawArrColSource.cast(Object[].class, Integer.class); + }); + assertEquals( + "Cannot convert ColumnSource componentType of type java.lang.String to java.lang.Integer (for [Ljava.lang.String; / [Ljava.lang.Object;)", + castExceptionWithCompNoColName.getMessage()); + } + private static final class DummyUpdateGraph implements UpdateGraph { private final String name;