diff --git a/driver/src/main/java/org/neo4j/driver/Record.java b/driver/src/main/java/org/neo4j/driver/Record.java index cd662ecf4e..a76e5be281 100644 --- a/driver/src/main/java/org/neo4j/driver/Record.java +++ b/driver/src/main/java/org/neo4j/driver/Record.java @@ -129,6 +129,8 @@ public interface Record extends MapAccessorWithDefaultValue { * {@code null} value (this includes primitive types), an alternative constructor that excludes it must be * available. *
+ * The mapping only works for types with directly accessible constructors, not interfaces or abstract types. + *
* Types with generic parameters defined at the class level are not supported. However, constructor arguments with * specific types are permitted, see the {@code actors} parameter in the example above. *
diff --git a/driver/src/main/java/org/neo4j/driver/Value.java b/driver/src/main/java/org/neo4j/driver/Value.java index 6a7d872e51..744d41566d 100644 --- a/driver/src/main/java/org/neo4j/driver/Value.java +++ b/driver/src/main/java/org/neo4j/driver/Value.java @@ -545,11 +545,11 @@ public interface Value extends MapAccessor, MapAccessorWithDefaultValue { * *
+ * The mapping only works for types with directly accessible constructors, not interfaces or abstract types. + *
* Example with optional property (using the Neo4j Movies Database): *
* {@code diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java index 0211135a09..3588b7ffea 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/IntegerValue.java @@ -85,10 +85,21 @@ publicT as(Class targetClass) { return (T) Float.valueOf(asFloat()); } else if (targetClass.equals(Float.class)) { return targetClass.cast(asFloat()); + } else if (targetClass.equals(short.class)) { + return (T) Short.valueOf(asShort()); + } else if (targetClass.equals(Short.class)) { + return targetClass.cast(asShort()); } throw new Uncoercible(type().name(), targetClass.getCanonicalName()); } + private short asShort() { + if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) { + throw new LossyCoercion(type().name(), "Java short"); + } + return (short) val; + } + @Override public String toString() { return Long.toString(val); diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java index acabe97626..9809ac71ac 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/ListValue.java @@ -18,6 +18,7 @@ import static org.neo4j.driver.Values.ofObject; +import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.util.Arrays; import java.util.Iterator; @@ -26,6 +27,7 @@ import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.value.Uncoercible; +import org.neo4j.driver.exceptions.value.ValueException; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.internal.util.Extract; import org.neo4j.driver.types.Type; @@ -64,6 +66,22 @@ public List asList(Function mapFunction) { public T as(Class targetClass) { if (targetClass.isAssignableFrom(List.class)) { return targetClass.cast(asList()); + } else if (targetClass.isArray()) { + var componentType = targetClass.componentType(); + var array = Array.newInstance(componentType, values.length); + for (var i = 0; i < values.length; i++) { + Object value; + try { + value = values[i].as(componentType); + } catch (Throwable throwable) { + throw new ValueException( + "Failed to map LIST value to %s - an error occured while mapping the element at index %d" + .formatted(targetClass.getCanonicalName(), i), + throwable); + } + Array.set(array, i, value); + } + return targetClass.cast(array); } throw new Uncoercible(type().name(), targetClass.getCanonicalName()); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java b/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java index 75c92f03f7..49a17810d0 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java +++ b/driver/src/main/java/org/neo4j/driver/internal/value/StringValue.java @@ -17,6 +17,7 @@ package org.neo4j.driver.internal.value; import java.util.Objects; +import org.neo4j.driver.exceptions.value.LossyCoercion; import org.neo4j.driver.exceptions.value.Uncoercible; import org.neo4j.driver.internal.types.InternalTypeSystem; import org.neo4j.driver.types.Type; @@ -51,10 +52,17 @@ public String asString() { return val; } + @SuppressWarnings("unchecked") @Override public T as(Class targetClass) { if (targetClass.isAssignableFrom(String.class)) { return targetClass.cast(asString()); + } else if ((targetClass.isAssignableFrom(char.class) || targetClass.isAssignableFrom(Character.class))) { + if (val.length() == 1) { + return (T) Character.valueOf(val.charAt(0)); + } else { + throw new LossyCoercion(type().name(), targetClass.getCanonicalName()); + } } throw new Uncoercible(type().name(), targetClass.getCanonicalName()); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java index 6403b602f3..b071fd67f2 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/IntegerValueTest.java @@ -148,5 +148,7 @@ void shouldMapToType() { assertEquals(expected, value.as(Double.class)); assertEquals((float) expected, value.as(float.class)); assertEquals((float) expected, value.as(Float.class)); + assertEquals((short) expected, value.as(short.class)); + assertEquals((short) expected, value.as(Short.class)); } } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java index 97e5be2e80..2b528bfd45 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/ListValueTest.java @@ -18,9 +18,12 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.neo4j.driver.Values.value; +import java.time.LocalDateTime; +import java.util.Arrays; import java.util.Collection; import java.util.List; import org.junit.jupiter.api.Test; @@ -53,6 +56,107 @@ void shouldMapToType() { assertEquals(list, values.as(Object.class)); } + @Test + void shouldMapCharValuesToArray() { + var array = new char[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(char[].class)); + } + + @Test + void shouldMapBooleanValuesToArray() { + var array = new boolean[] {false}; + var values = Values.value(array); + assertArrayEquals(array, values.as(boolean[].class)); + } + + @Test + void shouldMapStringValuesToArray() { + var array = new String[] {"value"}; + var values = Values.value(array); + assertArrayEquals(array, values.as(String[].class)); + } + + @Test + void shouldMapLongValuesToArray() { + var array = new long[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(long[].class)); + assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class)); + assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class)); + var expectedFloats = new float[array.length]; + for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i]; + assertArrayEquals(expectedFloats, values.as(float[].class)); + } + + @Test + void shouldMapIntegerValuesToArray() { + var array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(int[].class)); + assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class)); + assertArrayEquals(Arrays.stream(array).mapToDouble(l -> (double) l).toArray(), values.as(double[].class)); + var expectedFloats = new float[array.length]; + for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i]; + assertArrayEquals(expectedFloats, values.as(float[].class)); + } + + @Test + void shouldMapShortValuesToArray() { + var array = new short[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(short[].class)); + } + + @Test + void shouldMapDoubleValuesToArray() { + var array = new double[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(double[].class)); + assertArrayEquals(Arrays.stream(array).mapToLong(l -> (long) l).toArray(), values.as(long[].class)); + assertArrayEquals(Arrays.stream(array).mapToInt(l -> (int) l).toArray(), values.as(int[].class)); + var expectedFloats = new float[array.length]; + for (var i = 0; i < array.length; i++) expectedFloats[i] = (float) array[i]; + assertArrayEquals(expectedFloats, values.as(float[].class)); + } + + @Test + void shouldMapFloatValuesToArray() { + var array = new float[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + var values = Values.value(array); + assertArrayEquals(array, values.as(float[].class)); + var expectedDoubles = new double[array.length]; + for (var i = 0; i < array.length; i++) expectedDoubles[i] = array[i]; + assertArrayEquals(expectedDoubles, values.as(double[].class)); + var expectedLongs = new long[array.length]; + for (var i = 0; i < array.length; i++) expectedLongs[i] = (long) array[i]; + assertArrayEquals(expectedLongs, values.as(long[].class)); + var expectedIntegers = new int[array.length]; + for (var i = 0; i < array.length; i++) expectedIntegers[i] = (int) array[i]; + assertArrayEquals(expectedIntegers, values.as(int[].class)); + } + + @Test + void shouldMapObjectsToArray() { + var string = "value"; + var localDateTime = LocalDateTime.now(); + var array = new Object[] {string, localDateTime}; + var values = Values.value(array); + assertArrayEquals(array, values.as(Object[].class)); + } + + @Test + void shouldMapMatrixValuesToArrays() { + var array = new long[10][10]; + for (var i = 0; i < array.length; i++) { + for (var j = 0; j < 10; j++) { + array[i][j] = i * j; + } + } + var values = Values.value(array); + assertArrayEquals(array, values.as(long[][].class)); + } + private ListValue listValue(Value... values) { return new ListValue(values); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java b/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java index 7dd5df8ea3..24e9bbda2b 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/value/StringValueTest.java @@ -92,9 +92,11 @@ void shouldHaveStringType() { @Test void shouldMapToType() { - var string = "value"; + var string = "0"; var values = Values.value(string); assertEquals(string, values.as(String.class)); + assertEquals(string.charAt(0), values.as(char.class)); + assertEquals(string.charAt(0), values.as(Character.class)); assertEquals(string, values.as(Serializable.class)); assertEquals(string, values.as(Comparable.class)); assertEquals(string, values.as(CharSequence.class));