diff --git a/Base/src/main/java/io/deephaven/base/MathUtil.java b/Base/src/main/java/io/deephaven/base/MathUtil.java
index 1f07a5923ef..555409a709b 100644
--- a/Base/src/main/java/io/deephaven/base/MathUtil.java
+++ b/Base/src/main/java/io/deephaven/base/MathUtil.java
@@ -8,6 +8,11 @@
*/
public class MathUtil {
+ /**
+ * The maximum power of 2.
+ */
+ public static final int MAX_POWER_OF_2 = 1 << 30;
+
/**
* Compute ceil(log2(x)). See {@link Integer#numberOfLeadingZeros(int)}.
*
@@ -108,4 +113,36 @@ public static int base10digits(int n) {
}
return base10guess;
}
+
+ /**
+ * Rounds up to the next power of 2 for {@code x}; if {@code x} is already a power of 2, {@code x} will be returned.
+ * Values outside the range {@code 1 <= x <= MAX_POWER_OF_2} will return {@code 1}.
+ *
+ *
+ * Equivalent to {@code Math.max(Integer.highestOneBit(x - 1) << 1, 1)}.
+ *
+ * @param x the value
+ * @return the next power of 2 for {@code x}
+ * @see #MAX_POWER_OF_2
+ */
+ public static int roundUpPowerOf2(int x) {
+ return Math.max(Integer.highestOneBit(x - 1) << 1, 1);
+ }
+
+ /**
+ * Rounds up to the next power of 2 for {@code size <= MAX_POWER_OF_2}, otherwise returns
+ * {@link ArrayUtil#MAX_ARRAY_SIZE}.
+ *
+ *
+ * Equivalent to {@code size <= MAX_POWER_OF_2 ? roundUpPowerOf2(size) : ArrayUtil.MAX_ARRAY_SIZE}.
+ *
+ * @param size the size
+ * @return the
+ * @see #MAX_POWER_OF_2
+ * @see #roundUpPowerOf2(int)
+ * @see ArrayUtil#MAX_ARRAY_SIZE
+ */
+ public static int roundUpArraySize(int size) {
+ return size <= MAX_POWER_OF_2 ? roundUpPowerOf2(size) : ArrayUtil.MAX_ARRAY_SIZE;
+ }
}
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/ByteRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/ByteRingBuffer.java
index ee8e47041cd..aede7521309 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/ByteRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/ByteRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class ByteRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
byte[] storage;
@@ -45,21 +43,13 @@ public ByteRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public ByteRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "ByteRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "ByteRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new byte[newCapacity];
+ storage = new byte[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "ByteRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "ByteRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final byte[] newStorage = new byte[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final byte[] newStorage = new byte[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/CharRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/CharRingBuffer.java
index a2d934e9537..68c58a866e7 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/CharRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/CharRingBuffer.java
@@ -3,7 +3,7 @@
//
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -16,8 +16,6 @@
* determination of storage indices through a mask operation.
*/
public class CharRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
char[] storage;
@@ -41,21 +39,13 @@ public CharRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public CharRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "CharRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "CharRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new char[newCapacity];
+ storage = new char[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -69,9 +59,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "CharRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "CharRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final char[] newStorage = new char[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final char[] newStorage = new char[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/DoubleRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/DoubleRingBuffer.java
index 3f0f5cfbe35..8e942f8937a 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/DoubleRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/DoubleRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class DoubleRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
double[] storage;
@@ -45,21 +43,13 @@ public DoubleRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public DoubleRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "DoubleRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "DoubleRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new double[newCapacity];
+ storage = new double[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "DoubleRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "DoubleRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final double[] newStorage = new double[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final double[] newStorage = new double[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/FloatRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/FloatRingBuffer.java
index 70cbded55ac..16c8751c447 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/FloatRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/FloatRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class FloatRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
float[] storage;
@@ -45,21 +43,13 @@ public FloatRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public FloatRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "FloatRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "FloatRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new float[newCapacity];
+ storage = new float[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "FloatRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "FloatRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final float[] newStorage = new float[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final float[] newStorage = new float[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/IntRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/IntRingBuffer.java
index 590014fa926..80a47f0a389 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/IntRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/IntRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class IntRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
int[] storage;
@@ -45,21 +43,13 @@ public IntRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public IntRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "IntRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "IntRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new int[newCapacity];
+ storage = new int[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "IntRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "IntRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final int[] newStorage = new int[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final int[] newStorage = new int[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/LongRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/LongRingBuffer.java
index 49a3203860a..de5f8df9c90 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/LongRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/LongRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class LongRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
long[] storage;
@@ -45,21 +43,13 @@ public LongRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public LongRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "LongRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "LongRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new long[newCapacity];
+ storage = new long[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "LongRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "LongRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final long[] newStorage = new long[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final long[] newStorage = new long[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/ObjectRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/ObjectRingBuffer.java
index e65291e23a6..ad93ddd1f43 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/ObjectRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/ObjectRingBuffer.java
@@ -9,7 +9,7 @@
import java.util.Arrays;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -22,8 +22,6 @@
* determination of storage indices through a mask operation.
*/
public class ObjectRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
T[] storage;
@@ -47,21 +45,13 @@ public ObjectRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public ObjectRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "ObjectRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "ObjectRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = (T[]) new Object[newCapacity];
+ storage = (T[]) new Object[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -75,9 +65,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "ObjectRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "ObjectRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final T[] newStorage = (T[]) new Object[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final T[] newStorage = (T[]) new Object[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/main/java/io/deephaven/base/ringbuffer/ShortRingBuffer.java b/Base/src/main/java/io/deephaven/base/ringbuffer/ShortRingBuffer.java
index d074fab3431..89619fb839c 100644
--- a/Base/src/main/java/io/deephaven/base/ringbuffer/ShortRingBuffer.java
+++ b/Base/src/main/java/io/deephaven/base/ringbuffer/ShortRingBuffer.java
@@ -7,7 +7,7 @@
// @formatter:off
package io.deephaven.base.ringbuffer;
-import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
import io.deephaven.base.verify.Assert;
import java.io.Serializable;
@@ -20,8 +20,6 @@
* determination of storage indices through a mask operation.
*/
public class ShortRingBuffer implements Serializable {
- /** Maximum capacity is the highest power of two that can be allocated (i.e. <= than ArrayUtil.MAX_ARRAY_SIZE). */
- static final int RING_BUFFER_MAX_CAPACITY = Integer.highestOneBit(ArrayUtil.MAX_ARRAY_SIZE);
static final long FIXUP_THRESHOLD = 1L << 62;
final boolean growable;
short[] storage;
@@ -45,21 +43,13 @@ public ShortRingBuffer(int capacity) {
* @param growable whether to allow growth when the buffer is full.
*/
public ShortRingBuffer(int capacity, boolean growable) {
- Assert.leq(capacity, "ShortRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(capacity, "ShortRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
this.growable = growable;
// use next larger power of 2 for our storage
- final int newCapacity;
- if (capacity < 2) {
- // sensibly handle the size=0 and size=1 cases
- newCapacity = 1;
- } else {
- newCapacity = Integer.highestOneBit(capacity - 1) << 1;
- }
-
// reset the data structure members
- storage = new short[newCapacity];
+ storage = new short[MathUtil.roundUpPowerOf2(capacity)];
mask = storage.length - 1;
tail = head = 0;
}
@@ -73,9 +63,9 @@ protected void grow(int increase) {
final int size = size();
final long newCapacity = (long) storage.length + increase;
// assert that we are not asking for the impossible
- Assert.leq(newCapacity, "ShortRingBuffer capacity", RING_BUFFER_MAX_CAPACITY);
+ Assert.leq(newCapacity, "ShortRingBuffer capacity", MathUtil.MAX_POWER_OF_2);
- final short[] newStorage = new short[Integer.highestOneBit((int) newCapacity - 1) << 1];
+ final short[] newStorage = new short[MathUtil.roundUpPowerOf2((int) newCapacity)];
// move the current data to the new buffer
copyRingBufferToArray(newStorage);
diff --git a/Base/src/test/java/io/deephaven/base/MathUtilTest.java b/Base/src/test/java/io/deephaven/base/MathUtilTest.java
index 2f9833d96a1..b29cc79d3ca 100644
--- a/Base/src/test/java/io/deephaven/base/MathUtilTest.java
+++ b/Base/src/test/java/io/deephaven/base/MathUtilTest.java
@@ -24,4 +24,43 @@ public void check(int a, int b, int expect) {
assertEquals(expect, MathUtil.gcd(-a, -b));
assertEquals(expect, MathUtil.gcd(-b, -a));
}
+
+ public void testRoundUpPowerOf2() {
+ pow2(0, 1);
+ pow2(1, 1);
+ pow2(2, 2);
+ for (int i = 2; i < 31; ++i) {
+ final int pow2 = 1 << i;
+ pow2(pow2, pow2);
+ pow2(pow2 - 1, pow2);
+ if (i < 30) {
+ pow2(pow2 + 1, pow2 * 2);
+ }
+ }
+ }
+
+ public void testRoundUpArraySize() {
+ arraySize(0, 1);
+ arraySize(1, 1);
+ arraySize(2, 2);
+ for (int i = 2; i < 31; ++i) {
+ final int pow2 = 1 << i;
+ arraySize(pow2, pow2);
+ arraySize(pow2 - 1, pow2);
+ if (i < 30) {
+ arraySize(pow2 + 1, pow2 * 2);
+ } else {
+ arraySize(pow2 + 1, ArrayUtil.MAX_ARRAY_SIZE);
+ }
+ }
+ arraySize(Integer.MAX_VALUE, ArrayUtil.MAX_ARRAY_SIZE);
+ }
+
+ public static void pow2(int newSize, int expectedSize) {
+ assertEquals(MathUtil.roundUpPowerOf2(newSize), expectedSize);
+ }
+
+ public static void arraySize(int newSize, int expectedSize) {
+ assertEquals(MathUtil.roundUpArraySize(newSize), expectedSize);
+ }
}
diff --git a/buildSrc/src/main/groovy/Classpaths.groovy b/buildSrc/src/main/groovy/Classpaths.groovy
index 3624a77f408..797458664a1 100644
--- a/buildSrc/src/main/groovy/Classpaths.groovy
+++ b/buildSrc/src/main/groovy/Classpaths.groovy
@@ -109,7 +109,7 @@ class Classpaths {
static final String JACKSON_GROUP = 'com.fasterxml.jackson'
static final String JACKSON_NAME = 'jackson-bom'
- static final String JACKSON_VERSION = '2.14.1'
+ static final String JACKSON_VERSION = '2.17.0'
static final String SSLCONTEXT_GROUP = 'io.github.hakky54'
static final String SSLCONTEXT_VERSION = '8.1.1'
diff --git a/engine/chunk/src/main/java/io/deephaven/chunk/util/hashing/ObjectChunkDeepEquals.java b/engine/chunk/src/main/java/io/deephaven/chunk/util/hashing/ObjectChunkDeepEquals.java
new file mode 100644
index 00000000000..985aca636f1
--- /dev/null
+++ b/engine/chunk/src/main/java/io/deephaven/chunk/util/hashing/ObjectChunkDeepEquals.java
@@ -0,0 +1,264 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+// ****** AUTO-GENERATED CLASS - DO NOT EDIT MANUALLY
+// ****** Edit CharChunkEquals and run "./gradlew replicateHashing" to regenerate
+//
+// @formatter:off
+package io.deephaven.chunk.util.hashing;
+
+import java.util.Objects;
+
+import io.deephaven.chunk.*;
+import io.deephaven.chunk.attributes.Any;
+import io.deephaven.chunk.attributes.ChunkPositions;
+
+// region name
+public class ObjectChunkDeepEquals implements ChunkEquals {
+ public static ObjectChunkDeepEquals INSTANCE = new ObjectChunkDeepEquals();
+ // endregion name
+
+ public static boolean equalReduce(ObjectChunk lhs, ObjectChunk rhs) {
+ if (lhs.size() != rhs.size()) {
+ return false;
+ }
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ if (!eq(lhs.get(ii), rhs.get(ii))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static int firstDifference(ObjectChunk lhs, ObjectChunk rhs) {
+ int ii = 0;
+ for (ii = 0; ii < lhs.size() && ii < rhs.size(); ++ii) {
+ if (!eq(lhs.get(ii), rhs.get(ii))) {
+ return ii;
+ }
+ }
+ return ii;
+ }
+
+ private static void equal(ObjectChunk lhs, ObjectChunk rhs,
+ WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, eq(lhs.get(ii), rhs.get(ii)));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ private static void equalNext(ObjectChunk chunk, WritableBooleanChunk destination) {
+ for (int ii = 0; ii < chunk.size() - 1; ++ii) {
+ destination.set(ii, eq(chunk.get(ii), chunk.get(ii + 1)));
+ }
+ destination.setSize(chunk.size() - 1);
+ }
+
+ private static void equal(ObjectChunk lhs, Object rhs, WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, eq(lhs.get(ii), rhs));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ public static void notEqual(ObjectChunk lhs, ObjectChunk rhs,
+ WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, neq(lhs.get(ii), rhs.get(ii)));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ public static void notEqual(ObjectChunk lhs, Object rhs, WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, neq(lhs.get(ii), rhs));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ private static void andEqual(ObjectChunk lhs, ObjectChunk rhs,
+ WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, destination.get(ii) && eq(lhs.get(ii), rhs.get(ii)));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ private static void andNotEqual(ObjectChunk lhs, ObjectChunk rhs,
+ WritableBooleanChunk destination) {
+ for (int ii = 0; ii < lhs.size(); ++ii) {
+ destination.set(ii, destination.get(ii) && neq(lhs.get(ii), rhs.get(ii)));
+ }
+ destination.setSize(lhs.size());
+ }
+
+ private static void andEqualNext(ObjectChunk chunk, WritableBooleanChunk destination) {
+ for (int ii = 0; ii < chunk.size() - 1; ++ii) {
+ destination.set(ii, destination.get(ii) && eq(chunk.get(ii), chunk.get(ii + 1)));
+ }
+ destination.setSize(chunk.size() - 1);
+ }
+
+ private static void equalPairs(IntChunk chunkPositionsToCheckForEquality,
+ ObjectChunk valuesChunk, WritableBooleanChunk destinations) {
+ final int pairCount = chunkPositionsToCheckForEquality.size() / 2;
+ for (int ii = 0; ii < pairCount; ++ii) {
+ final int firstPosition = chunkPositionsToCheckForEquality.get(ii * 2);
+ final int secondPosition = chunkPositionsToCheckForEquality.get(ii * 2 + 1);
+ final boolean equals = eq(valuesChunk.get(firstPosition), valuesChunk.get(secondPosition));
+ destinations.set(ii, equals);
+ }
+ destinations.setSize(pairCount);
+ }
+
+ private static void andEqualPairs(IntChunk chunkPositionsToCheckForEquality,
+ ObjectChunk valuesChunk, WritableBooleanChunk destinations) {
+ final int pairCount = chunkPositionsToCheckForEquality.size() / 2;
+ for (int ii = 0; ii < pairCount; ++ii) {
+ if (destinations.get(ii)) {
+ final int firstPosition = chunkPositionsToCheckForEquality.get(ii * 2);
+ final int secondPosition = chunkPositionsToCheckForEquality.get(ii * 2 + 1);
+ final boolean equals = eq(valuesChunk.get(firstPosition), valuesChunk.get(secondPosition));
+ destinations.set(ii, equals);
+ }
+ }
+ }
+
+ private static void equalPermuted(IntChunk lhsPositions, IntChunk rhsPositions,
+ ObjectChunk lhs, ObjectChunk rhs, WritableBooleanChunk destinations) {
+ for (int ii = 0; ii < lhsPositions.size(); ++ii) {
+ final int lhsPosition = lhsPositions.get(ii);
+ final int rhsPosition = rhsPositions.get(ii);
+ final boolean equals = eq(lhs.get(lhsPosition), rhs.get(rhsPosition));
+ destinations.set(ii, equals);
+ }
+ destinations.setSize(lhsPositions.size());
+ }
+
+ private static void andEqualPermuted(IntChunk lhsPositions, IntChunk rhsPositions,
+ ObjectChunk lhs, ObjectChunk rhs, WritableBooleanChunk destinations) {
+ for (int ii = 0; ii < lhsPositions.size(); ++ii) {
+ if (destinations.get(ii)) {
+ final int lhsPosition = lhsPositions.get(ii);
+ final int rhsPosition = rhsPositions.get(ii);
+ final boolean equals = eq(lhs.get(lhsPosition), rhs.get(rhsPosition));
+ destinations.set(ii, equals);
+ }
+ }
+ destinations.setSize(lhsPositions.size());
+ }
+
+ private static void equalLhsPermuted(IntChunk lhsPositions, ObjectChunk lhs,
+ ObjectChunk rhs, WritableBooleanChunk destinations) {
+ for (int ii = 0; ii < lhsPositions.size(); ++ii) {
+ final int lhsPosition = lhsPositions.get(ii);
+ final boolean equals = eq(lhs.get(lhsPosition), rhs.get(ii));
+ destinations.set(ii, equals);
+ }
+ destinations.setSize(lhsPositions.size());
+ }
+
+ private static void andEqualLhsPermuted(IntChunk lhsPositions, ObjectChunk lhs,
+ ObjectChunk rhs, WritableBooleanChunk destinations) {
+ for (int ii = 0; ii < lhsPositions.size(); ++ii) {
+ if (destinations.get(ii)) {
+ final int lhsPosition = lhsPositions.get(ii);
+ final boolean equals = eq(lhs.get(lhsPosition), rhs.get(ii));
+ destinations.set(ii, equals);
+ }
+ }
+ destinations.setSize(lhsPositions.size());
+ }
+
+ @Override
+ public boolean equalReduce(Chunk extends Any> lhs, Chunk extends Any> rhs) {
+ return equalReduce(lhs.asObjectChunk(), rhs.asObjectChunk());
+ }
+
+ @Override
+ public void equal(Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ equal(lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ public static void equal(Chunk extends Any> lhs, Object rhs, WritableBooleanChunk destination) {
+ equal(lhs.asObjectChunk(), rhs, destination);
+ }
+
+ @Override
+ public void equalNext(Chunk extends Any> chunk, WritableBooleanChunk destination) {
+ equalNext(chunk.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void andEqual(Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ andEqual(lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void andEqualNext(Chunk extends Any> chunk, WritableBooleanChunk destination) {
+ andEqualNext(chunk.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void equalPermuted(IntChunk lhsPositions, IntChunk rhsPositions,
+ Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ equalPermuted(lhsPositions, rhsPositions, lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void equalLhsPermuted(IntChunk lhsPositions, Chunk extends Any> lhs,
+ Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ equalLhsPermuted(lhsPositions, lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void andEqualPermuted(IntChunk lhsPositions, IntChunk rhsPositions,
+ Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ andEqualPermuted(lhsPositions, rhsPositions, lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void andEqualLhsPermuted(IntChunk lhsPositions, Chunk extends Any> lhs,
+ Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ andEqualLhsPermuted(lhsPositions, lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void notEqual(Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ notEqual(lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ public static void notEqual(Chunk extends Any> lhs, Object rhs, WritableBooleanChunk destination) {
+ notEqual(lhs.asObjectChunk(), rhs, destination);
+ }
+
+ @Override
+ public void andNotEqual(Chunk extends Any> lhs, Chunk extends Any> rhs, WritableBooleanChunk destination) {
+ andNotEqual(lhs.asObjectChunk(), rhs.asObjectChunk(), destination);
+ }
+
+ @Override
+ public void equalPairs(IntChunk chunkPositionsToCheckForEquality, Chunk extends Any> valuesChunk,
+ WritableBooleanChunk destinations) {
+ equalPairs(chunkPositionsToCheckForEquality, valuesChunk.asObjectChunk(), destinations);
+ }
+
+ @Override
+ public void andEqualPairs(IntChunk chunkPositionsToCheckForEquality,
+ Chunk extends Any> valuesChunk, WritableBooleanChunk destinations) {
+ andEqualPairs(chunkPositionsToCheckForEquality, valuesChunk.asObjectChunk(), destinations);
+ }
+
+ // region eq
+ static private boolean eq(Object lhs, Object rhs) {
+ return Objects.deepEquals(lhs, rhs);
+ }
+ // endregion eq
+
+ // region neq
+ static private boolean neq(Object lhs, Object rhs) {
+ return !eq(lhs, rhs);
+ }
+ // endregion neq
+}
diff --git a/engine/processor/build.gradle b/engine/processor/build.gradle
index 6e7740b5736..e83b218b6f7 100644
--- a/engine/processor/build.gradle
+++ b/engine/processor/build.gradle
@@ -8,6 +8,8 @@ dependencies {
api project(':qst-type')
api project(':engine-chunk')
+ Classpaths.inheritImmutables(project)
+
Classpaths.inheritJUnitPlatform(project)
Classpaths.inheritAssertJ(project)
testImplementation 'org.junit.jupiter:junit-jupiter'
diff --git a/engine/processor/src/main/java/io/deephaven/processor/NamedObjectProcessor.java b/engine/processor/src/main/java/io/deephaven/processor/NamedObjectProcessor.java
new file mode 100644
index 00000000000..96703818349
--- /dev/null
+++ b/engine/processor/src/main/java/io/deephaven/processor/NamedObjectProcessor.java
@@ -0,0 +1,86 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.processor;
+
+import io.deephaven.annotations.BuildableStyle;
+import io.deephaven.qst.type.Type;
+import org.immutables.value.Value.Check;
+import org.immutables.value.Value.Immutable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@Immutable
+@BuildableStyle
+public abstract class NamedObjectProcessor {
+
+ public static Builder builder() {
+ return ImmutableNamedObjectProcessor.builder();
+ }
+
+ public static NamedObjectProcessor of(ObjectProcessor super T> processor, String... names) {
+ return NamedObjectProcessor.builder().processor(processor).addNames(names).build();
+ }
+
+ public static NamedObjectProcessor of(ObjectProcessor super T> processor, Iterable names) {
+ return NamedObjectProcessor.builder().processor(processor).addAllNames(names).build();
+ }
+
+ /**
+ * The name for each output of {@link #processor()}.
+ */
+ public abstract List names();
+
+ /**
+ * The object processor.
+ */
+ public abstract ObjectProcessor super T> processor();
+
+ public interface Builder {
+ Builder processor(ObjectProcessor super T> processor);
+
+ Builder addNames(String element);
+
+ Builder addNames(String... elements);
+
+ Builder addAllNames(Iterable elements);
+
+ NamedObjectProcessor build();
+ }
+
+ public interface Provider extends ObjectProcessor.Provider {
+
+ /**
+ * The name for each output of the processors. Equivalent to the named processors'
+ * {@link NamedObjectProcessor#names()}.
+ *
+ * @return the names
+ */
+ List names();
+
+ /**
+ * Creates a named object processor that can process the {@code inputType}. This will successfully create a
+ * named processor when {@code inputType} is one of, or extends from one of, {@link #inputTypes()}. Otherwise,
+ * an {@link IllegalArgumentException} will be thrown. Equivalent to
+ * {@code NamedObjectProcessor.of(processor(inputType), names())}.
+ *
+ * @param inputType the input type
+ * @return the object processor
+ * @param the input type
+ */
+ default NamedObjectProcessor super T> named(Type inputType) {
+ return NamedObjectProcessor.of(processor(inputType), names());
+ }
+ }
+
+ @Check
+ final void checkSizes() {
+ if (names().size() != processor().outputSize()) {
+ throw new IllegalArgumentException(
+ String.format("Unmatched sizes; names().size()=%d, processor().outputSize()=%d",
+ names().size(), processor().outputSize()));
+ }
+ }
+}
diff --git a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessor.java b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessor.java
index fcb166ec5f0..70e42e14f05 100644
--- a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessor.java
+++ b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessor.java
@@ -19,8 +19,8 @@
import io.deephaven.qst.type.ShortType;
import io.deephaven.qst.type.Type;
-import java.time.Instant;
import java.util.List;
+import java.util.Set;
/**
* An interface for processing data from one or more input objects into output chunks on a 1-to-1 input record to output
@@ -141,6 +141,15 @@ static ChunkType chunkType(Type> type) {
return ObjectProcessorTypes.of(type);
}
+ /**
+ * The number of outputs. Equivalent to {@code outputTypes().size()}.
+ *
+ * @return the number of outputs
+ */
+ default int outputSize() {
+ return outputTypes().size();
+ }
+
/**
* The logical output types {@code this} instance processes. The size and types correspond to the expected size and
* {@link io.deephaven.chunk.ChunkType chunk types} for {@link #processAll(ObjectChunk, List)} as specified by
@@ -168,4 +177,44 @@ static ChunkType chunkType(Type> type) {
* at least {@code in.size()}
*/
void processAll(ObjectChunk extends T, ?> in, List> out);
+
+ /**
+ * An abstraction over {@link ObjectProcessor} that provides the same logical object processor for different input
+ * types.
+ */
+ interface Provider {
+
+ /**
+ * The supported input types for {@link #processor(Type)}.
+ *
+ * @return the supported input types
+ */
+ Set> inputTypes();
+
+ /**
+ * The output types for the processors. Equivalent to the processors' {@link ObjectProcessor#outputTypes()}.
+ *
+ * @return the output types
+ */
+ List> outputTypes();
+
+ /**
+ * The number of output types for the processors. Equivalent to the processors'
+ * {@link ObjectProcessor#outputSize()}.
+ *
+ * @return the number of output types
+ */
+ int outputSize();
+
+ /**
+ * Creates an object processor that can process the {@code inputType}. This will successfully create a processor
+ * when {@code inputType} is one of, or extends from one of, {@link #inputTypes()}. Otherwise, an
+ * {@link IllegalArgumentException} will be thrown.
+ *
+ * @param inputType the input type
+ * @return the object processor
+ * @param the input type
+ */
+ ObjectProcessor super T> processor(Type inputType);
+ }
}
diff --git a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorRowLimited.java b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorRowLimited.java
index 7dfd03f1249..53d2192e517 100644
--- a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorRowLimited.java
+++ b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorRowLimited.java
@@ -48,6 +48,11 @@ int rowLimit() {
return rowLimit;
}
+ @Override
+ public int outputSize() {
+ return delegate.outputSize();
+ }
+
@Override
public List> outputTypes() {
return delegate.outputTypes();
diff --git a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorStrict.java b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorStrict.java
index fe1f69277f4..30c84f38aea 100644
--- a/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorStrict.java
+++ b/engine/processor/src/main/java/io/deephaven/processor/ObjectProcessorStrict.java
@@ -27,6 +27,16 @@ static ObjectProcessor create(ObjectProcessor delegate) {
ObjectProcessorStrict(ObjectProcessor delegate) {
this.delegate = Objects.requireNonNull(delegate);
this.outputTypes = List.copyOf(delegate.outputTypes());
+ if (delegate.outputSize() != outputTypes.size()) {
+ throw new IllegalArgumentException(
+ String.format("Inconsistent size. delegate.outputSize()=%d, delegate.outputTypes().size()=%d",
+ delegate.outputSize(), outputTypes.size()));
+ }
+ }
+
+ @Override
+ public int outputSize() {
+ return delegate.outputSize();
}
@Override
@@ -40,12 +50,13 @@ public List> outputTypes() {
@Override
public void processAll(ObjectChunk extends T, ?> in, List> out) {
- final int numColumns = delegate.outputTypes().size();
+ final int numColumns = delegate.outputSize();
if (numColumns != out.size()) {
throw new IllegalArgumentException(String.format(
- "Improper number of out chunks. Expected delegate.outputTypes().size() == out.size(). delegate.outputTypes().size()=%d, out.size()=%d",
+ "Improper number of out chunks. Expected delegate.outputSize() == out.size(). delegate.outputSize()=%d, out.size()=%d",
numColumns, out.size()));
}
+ final List> delegateOutputTypes = delegate.outputTypes();
final int[] originalSizes = new int[numColumns];
for (int chunkIx = 0; chunkIx < numColumns; ++chunkIx) {
final WritableChunk> chunk = out.get(chunkIx);
@@ -54,7 +65,7 @@ public void processAll(ObjectChunk extends T, ?> in, List> ou
"out chunk does not have enough remaining capacity. chunkIx=%d, in.size()=%d, chunk.size()=%d, chunk.capacity()=%d",
chunkIx, in.size(), chunk.size(), chunk.capacity()));
}
- final Type> type = delegate.outputTypes().get(chunkIx);
+ final Type> type = delegateOutputTypes.get(chunkIx);
final ChunkType expectedChunkType = ObjectProcessor.chunkType(type);
final ChunkType actualChunkType = chunk.getChunkType();
if (expectedChunkType != actualChunkType) {
diff --git a/engine/processor/src/test/java/io/deephaven/processor/ObjectProcessorStrictTest.java b/engine/processor/src/test/java/io/deephaven/processor/ObjectProcessorStrictTest.java
index eb79ed25583..562aeb368dc 100644
--- a/engine/processor/src/test/java/io/deephaven/processor/ObjectProcessorStrictTest.java
+++ b/engine/processor/src/test/java/io/deephaven/processor/ObjectProcessorStrictTest.java
@@ -103,7 +103,7 @@ public void testIncorrectChunkType() {
}
@Test
- public void testNotEnoughOutputSize() {
+ public void testNotEnoughOutputOutputSize() {
ObjectProcessor delegate = ObjectProcessor.noop(List.of(Type.intType()), false);
ObjectProcessor strict = ObjectProcessor.strict(delegate);
try (
@@ -172,6 +172,11 @@ public void testBadDelegateOutputTypes() {
ObjectProcessor strict = ObjectProcessor.strict(new ObjectProcessor<>() {
private final List> outputTypes = new ArrayList<>(List.of(Type.intType()));
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
@Override
public List> outputTypes() {
try {
@@ -220,4 +225,30 @@ public void processAll(ObjectChunk, ?> in, List> out) {
}
}
}
+
+ @Test
+ public void testBadDelegateOutputSize() {
+ try {
+ ObjectProcessor.strict(new ObjectProcessor<>() {
+ @Override
+ public int outputSize() {
+ return 2;
+ }
+
+ @Override
+ public List> outputTypes() {
+ return List.of(Type.intType());
+ }
+
+ @Override
+ public void processAll(ObjectChunk, ?> in, List> out) {
+ // ignore
+ }
+ });
+ failBecauseExceptionWasNotThrown(IllegalAccessException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessageContaining(
+ "Inconsistent size. delegate.outputSize()=2, delegate.outputTypes().size()=1");
+ }
+ }
}
diff --git a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ring/RingTableTools.java b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ring/RingTableTools.java
index 80bd1be94d7..45495357d15 100644
--- a/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ring/RingTableTools.java
+++ b/engine/table/src/main/java/io/deephaven/engine/table/impl/sources/ring/RingTableTools.java
@@ -3,6 +3,9 @@
//
package io.deephaven.engine.table.impl.sources.ring;
+import io.deephaven.base.ArrayUtil;
+import io.deephaven.base.MathUtil;
+import io.deephaven.base.verify.Require;
import io.deephaven.engine.context.ExecutionContext;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableUpdate;
@@ -43,6 +46,7 @@ public static Table of(Table parent, int capacity) {
* @return the ring table
*/
public static Table of(Table parent, int capacity, boolean initialize) {
+ Require.leq(capacity, "capacity", ArrayUtil.MAX_ARRAY_SIZE);
return QueryPerformanceRecorder.withNugget("RingTableTools.of", () -> {
final BaseTable> baseTable = (BaseTable>) parent.coalesce();
final OperationSnapshotControl snapshotControl =
@@ -56,7 +60,7 @@ public static Table of(Table parent, int capacity, boolean initialize) {
* re-indexed, with an additional {@link Table#tail(long)} to restructure for {@code capacity}.
*
*
- * Logically equivalent to {@code of(parent, Integer.highestOneBit(capacity - 1) << 1, initialize).tail(capacity)}.
+ * Logically equivalent to {@code of(parent, MathUtil.roundUpPowerOf2(capacity), initialize).tail(capacity)}.
*
*
* This setup may be useful when consumers need to maximize random access fill speed from a ring table.
@@ -66,11 +70,12 @@ public static Table of(Table parent, int capacity, boolean initialize) {
* @param initialize if the resulting table should source initial data from the snapshot of {@code parent}
* @return the ring table
* @see #of(Table, int, boolean)
+ * @see MathUtil#roundUpPowerOf2(int)
*/
public static Table of2(Table parent, int capacity, boolean initialize) {
+ Require.leq(capacity, "capacity", MathUtil.MAX_POWER_OF_2);
return QueryPerformanceRecorder.withNugget("RingTableTools.of2", () -> {
- // todo: there is probably a better way to do this
- final int capacityPowerOf2 = capacity == 1 ? 1 : Integer.highestOneBit(capacity - 1) << 1;
+ final int capacityPowerOf2 = MathUtil.roundUpPowerOf2(capacity);
final BaseTable> baseTable = (BaseTable>) parent.coalesce();
final OperationSnapshotControl snapshotControl =
baseTable.createSnapshotControlIfRefreshing(OperationSnapshotControl::new);
diff --git a/extensions/bson-jackson/build.gradle b/extensions/bson-jackson/build.gradle
new file mode 100644
index 00000000000..660ef1b207f
--- /dev/null
+++ b/extensions/bson-jackson/build.gradle
@@ -0,0 +1,23 @@
+plugins {
+ id 'java-library'
+ id 'io.deephaven.project.register'
+}
+
+dependencies {
+ api project(':extensions-json-jackson')
+ api project(':engine-processor')
+ api 'de.undercouch:bson4jackson:2.15.1'
+
+ Classpaths.inheritImmutables(project)
+ compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+
+ Classpaths.inheritJacksonPlatform(project, 'testImplementation')
+ Classpaths.inheritJUnitPlatform(project)
+ Classpaths.inheritAssertJ(project)
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation 'com.fasterxml.jackson.core:jackson-databind'
+}
+
+test {
+ useJUnitPlatform()
+}
diff --git a/extensions/bson-jackson/gradle.properties b/extensions/bson-jackson/gradle.properties
new file mode 100644
index 00000000000..c186bbfdde1
--- /dev/null
+++ b/extensions/bson-jackson/gradle.properties
@@ -0,0 +1 @@
+io.deephaven.project.ProjectType=JAVA_PUBLIC
diff --git a/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonConfiguration.java b/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonConfiguration.java
new file mode 100644
index 00000000000..663a7e02671
--- /dev/null
+++ b/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonConfiguration.java
@@ -0,0 +1,30 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.bson.jackson;
+
+import com.fasterxml.jackson.core.ObjectCodec;
+import de.undercouch.bson4jackson.BsonFactory;
+
+import java.lang.reflect.InvocationTargetException;
+
+final class JacksonBsonConfiguration {
+ private static final BsonFactory DEFAULT_FACTORY;
+
+ static {
+ // We'll attach an ObjectMapper if it's on the classpath, this allows parsing of AnyOptions
+ ObjectCodec objectCodec = null;
+ try {
+ final Class> clazz = Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
+ objectCodec = (ObjectCodec) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException
+ | InvocationTargetException e) {
+ // ignore
+ }
+ DEFAULT_FACTORY = new BsonFactory(objectCodec);
+ }
+
+ static BsonFactory defaultFactory() {
+ return DEFAULT_FACTORY;
+ }
+}
diff --git a/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonProvider.java b/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonProvider.java
new file mode 100644
index 00000000000..567d3a94bd4
--- /dev/null
+++ b/extensions/bson-jackson/src/main/java/io/deephaven/bson/jackson/JacksonBsonProvider.java
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.bson.jackson;
+
+import de.undercouch.bson4jackson.BsonFactory;
+import io.deephaven.json.Value;
+import io.deephaven.json.jackson.JacksonProvider;
+
+public final class JacksonBsonProvider {
+
+ /**
+ * Creates a jackson BSON provider using a default factory.
+ *
+ * @param options the object options
+ * @return the jackson BSON provider
+ * @see #of(Value, BsonFactory)
+ */
+ public static JacksonProvider of(Value options) {
+ return of(options, JacksonBsonConfiguration.defaultFactory());
+ }
+
+ /**
+ * Creates a jackson BSON provider using the provided {@code factory}.
+ *
+ * @param options the object options
+ * @param factory the jackson BSON factory
+ * @return the jackson BSON provider
+ */
+ public static JacksonProvider of(Value options, BsonFactory factory) {
+ return JacksonProvider.of(options, factory);
+ }
+}
diff --git a/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/BsonTest.java b/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/BsonTest.java
new file mode 100644
index 00000000000..1ac72354c1a
--- /dev/null
+++ b/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/BsonTest.java
@@ -0,0 +1,39 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.bson.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.undercouch.bson4jackson.BsonFactory;
+import io.deephaven.chunk.IntChunk;
+import io.deephaven.chunk.ObjectChunk;
+import io.deephaven.json.IntValue;
+import io.deephaven.json.ObjectValue;
+import io.deephaven.json.StringValue;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static io.deephaven.bson.jackson.TestHelper.parse;
+
+public class BsonTest {
+
+ private static final ObjectValue OBJECT_NAME_AGE_FIELD = ObjectValue.builder()
+ .putFields("name", StringValue.standard())
+ .putFields("age", IntValue.standard())
+ .build();
+
+ @Test
+ void bson() throws IOException {
+ final byte[] bsonExample = new ObjectMapper(new BsonFactory()).writeValueAsBytes(Map.of(
+ "name", "foo",
+ "age", 42));
+ parse(
+ JacksonBsonProvider.of(OBJECT_NAME_AGE_FIELD).bytesProcessor(),
+ List.of(bsonExample),
+ ObjectChunk.chunkWrap(new String[] {"foo"}),
+ IntChunk.chunkWrap(new int[] {42}));
+ }
+}
diff --git a/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/TestHelper.java b/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/TestHelper.java
new file mode 100644
index 00000000000..70564ee2e5c
--- /dev/null
+++ b/extensions/bson-jackson/src/test/java/io/deephaven/bson/jackson/TestHelper.java
@@ -0,0 +1,73 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.bson.jackson;
+
+import io.deephaven.chunk.Chunk;
+import io.deephaven.chunk.ChunkType;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableObjectChunk;
+import io.deephaven.chunk.attributes.Any;
+import io.deephaven.chunk.util.hashing.ChunkEquals;
+import io.deephaven.chunk.util.hashing.ObjectChunkDeepEquals;
+import io.deephaven.processor.ObjectProcessor;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestHelper {
+
+ public static void parse(ObjectProcessor super T> processor, List rows, Chunk>... expectedCols)
+ throws IOException {
+ final List> out = processor
+ .outputTypes()
+ .stream()
+ .map(ObjectProcessor::chunkType)
+ .map(x -> x.makeWritableChunk(rows.size()))
+ .collect(Collectors.toList());
+ try {
+ assertThat(out.size()).isEqualTo(expectedCols.length);
+ assertThat(out.stream().map(Chunk::getChunkType).collect(Collectors.toList()))
+ .isEqualTo(Stream.of(expectedCols).map(Chunk::getChunkType).collect(Collectors.toList()));
+ for (WritableChunk> wc : out) {
+ wc.setSize(0);
+ }
+ try (final WritableObjectChunk in = WritableObjectChunk.makeWritableChunk(rows.size())) {
+ int i = 0;
+ for (T input : rows) {
+ in.set(i, input);
+ ++i;
+ }
+ try {
+ processor.processAll(in, out);
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ }
+ }
+ for (int i = 0; i < expectedCols.length; ++i) {
+ check(out.get(i), expectedCols[i]);
+ }
+ } finally {
+ for (WritableChunk> wc : out) {
+ wc.close();
+ }
+ }
+ }
+
+ static void check(Chunk> actual, Chunk> expected) {
+ assertThat(actual.getChunkType()).isEqualTo(expected.getChunkType());
+ assertThat(actual.size()).isEqualTo(expected.size());
+ assertThat(getChunkEquals(actual).equalReduce(actual, expected)).isTrue();
+ }
+
+ private static ChunkEquals getChunkEquals(Chunk> actual) {
+ return actual.getChunkType() == ChunkType.Object
+ ? ObjectChunkDeepEquals.INSTANCE
+ : ChunkEquals.makeEqual(actual.getChunkType());
+ }
+}
diff --git a/extensions/json-jackson/build.gradle b/extensions/json-jackson/build.gradle
new file mode 100644
index 00000000000..bd18f5a88b4
--- /dev/null
+++ b/extensions/json-jackson/build.gradle
@@ -0,0 +1,34 @@
+plugins {
+ id 'java-library'
+ id 'io.deephaven.project.register'
+}
+
+dependencies {
+ api project(':engine-processor')
+
+ Classpaths.inheritJacksonPlatform(project, 'api')
+ Classpaths.inheritJacksonPlatform(project, 'testImplementation')
+
+ api 'com.fasterxml.jackson.core:jackson-core'
+ // https://github.com/FasterXML/jackson-core/issues/1229
+ implementation 'ch.randelshofer:fastdoubleparser:1.0.0'
+
+ api project(':extensions-json')
+
+ implementation project(':table-api') // only needs NameValidator, might be worth refactoring?
+
+ implementation project(':engine-query-constants')
+ implementation project(':engine-time')
+ Classpaths.inheritImmutables(project)
+ Classpaths.inheritAutoService(project)
+ compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+
+ Classpaths.inheritJUnitPlatform(project)
+ Classpaths.inheritAssertJ(project)
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation 'com.fasterxml.jackson.core:jackson-databind'
+}
+
+test {
+ useJUnitPlatform()
+}
diff --git a/extensions/json-jackson/gradle.properties b/extensions/json-jackson/gradle.properties
new file mode 100644
index 00000000000..c186bbfdde1
--- /dev/null
+++ b/extensions/json-jackson/gradle.properties
@@ -0,0 +1 @@
+io.deephaven.project.ProjectType=JAVA_PUBLIC
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/AnyMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/AnyMixin.java
new file mode 100644
index 00000000000..acaa2cc9fe2
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/AnyMixin.java
@@ -0,0 +1,28 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.TreeNode;
+import io.deephaven.json.AnyValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+
+final class AnyMixin extends GenericObjectMixin {
+ public AnyMixin(AnyValue options, JsonFactory factory) {
+ super(factory, options, Type.ofCustom(TreeNode.class));
+ }
+
+ @Override
+ public TreeNode parseValue(JsonParser parser) throws IOException {
+ return parser.readValueAsTree();
+ }
+
+ @Override
+ public TreeNode parseMissing(JsonParser parser) throws IOException {
+ return parser.getCodec().missingNode();
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ArrayMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ArrayMixin.java
new file mode 100644
index 00000000000..9f7ecd147e8
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ArrayMixin.java
@@ -0,0 +1,112 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.json.ArrayValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class ArrayMixin extends Mixin {
+
+ private final Mixin> element;
+
+ public ArrayMixin(ArrayValue options, JsonFactory factory) {
+ super(factory, options);
+ element = Mixin.of(options.element(), factory);
+ }
+
+ @Override
+ public int outputSize() {
+ return element.outputSize();
+ }
+
+ @Override
+ public Stream> paths() {
+ return element.paths();
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return elementOutputTypes().map(Type::arrayType);
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new ArrayMixinProcessor();
+ }
+
+ private Stream extends Type>> elementOutputTypes() {
+ return element.outputTypesImpl();
+ }
+
+ private RepeaterProcessor elementRepeater() {
+ return element.repeaterProcessor();
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ // For example:
+ // double (element())
+ // double[] (processor())
+ // double[][] (repeater())
+ // return new ArrayOfArrayRepeaterProcessor(allowMissing, allowNull);
+ return new ValueInnerRepeaterProcessor(new ArrayMixinProcessor());
+ }
+
+ private class ArrayMixinProcessor implements ValueProcessor {
+
+ private final RepeaterProcessor elementProcessor;
+
+ ArrayMixinProcessor() {
+ this.elementProcessor = elementRepeater();
+ }
+
+ @Override
+ public void setContext(List> out) {
+ elementProcessor.setContext(out);
+ }
+
+ @Override
+ public void clearContext() {
+ elementProcessor.clearContext();
+ }
+
+ @Override
+ public int numColumns() {
+ return elementProcessor.numColumns();
+ }
+
+ @Override
+ public Stream> columnTypes() {
+ return elementProcessor.columnTypes();
+ }
+
+ @Override
+ public void processCurrentValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case START_ARRAY:
+ RepeaterProcessor.processArray(parser, elementProcessor);
+ return;
+ case VALUE_NULL:
+ checkNullAllowed(parser);
+ elementProcessor.processNullRepeater(parser);
+ return;
+ default:
+ throw unexpectedToken(parser);
+ }
+ }
+
+ @Override
+ public void processMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ elementProcessor.processMissingRepeater(parser);
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigDecimalMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigDecimalMixin.java
new file mode 100644
index 00000000000..6d2d1fd3a8c
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigDecimalMixin.java
@@ -0,0 +1,59 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.json.BigDecimalValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+
+final class BigDecimalMixin extends GenericObjectMixin {
+
+ public BigDecimalMixin(BigDecimalValue options, JsonFactory factory) {
+ super(factory, options, Type.ofCustom(BigDecimal.class));
+ }
+
+ @Override
+ public BigDecimal parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ case VALUE_NUMBER_FLOAT:
+ return parseFromNumber(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ @Override
+ public BigDecimal parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ private BigDecimal parseFromNumber(JsonParser parser) throws IOException {
+ checkNumberAllowed(parser);
+ return Parsing.parseDecimalAsBigDecimal(parser);
+ }
+
+ private BigDecimal parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return Parsing.parseStringAsBigDecimal(parser);
+ }
+
+ private BigDecimal parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(null);
+ }
+
+ private BigDecimal parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(null);
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigIntegerMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigIntegerMixin.java
new file mode 100644
index 00000000000..f2585108fd2
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BigIntegerMixin.java
@@ -0,0 +1,67 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.json.BigIntegerValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.math.BigInteger;
+
+final class BigIntegerMixin extends GenericObjectMixin {
+
+ public BigIntegerMixin(BigIntegerValue options, JsonFactory factory) {
+ super(factory, options, Type.ofCustom(BigInteger.class));
+ }
+
+ @Override
+ public BigInteger parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ return parseFromInt(parser);
+ case VALUE_NUMBER_FLOAT:
+ return parseFromDecimal(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ @Override
+ public BigInteger parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ private BigInteger parseFromInt(JsonParser parser) throws IOException {
+ checkNumberIntAllowed(parser);
+ return Parsing.parseIntAsBigInteger(parser);
+ }
+
+ private BigInteger parseFromDecimal(JsonParser parser) throws IOException {
+ checkDecimalAllowed(parser);
+ return Parsing.parseDecimalAsBigInteger(parser);
+ }
+
+ private BigInteger parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return allowDecimal()
+ ? Parsing.parseDecimalStringAsBigInteger(parser)
+ : Parsing.parseStringAsBigInteger(parser);
+ }
+
+ private BigInteger parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(null);
+ }
+
+ private BigInteger parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(null);
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BoolMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BoolMixin.java
new file mode 100644
index 00000000000..70a5ba39d17
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/BoolMixin.java
@@ -0,0 +1,168 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableByteChunk;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.json.BoolValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.BooleanUtils;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class BoolMixin extends Mixin {
+
+ private final Boolean onNull;
+ private final Boolean onMissing;
+ private final byte onNullByte;
+ private final byte onMissingByte;
+
+ public BoolMixin(BoolValue options, JsonFactory factory) {
+ super(factory, options);
+ onNull = options.onNull().orElse(null);
+ onMissing = options.onMissing().orElse(null);
+ onNullByte = BooleanUtils.booleanAsByte(onNull);
+ onMissingByte = BooleanUtils.booleanAsByte(onMissing);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.booleanType().boxedType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new BoolMixinProcessor();
+ }
+
+ private byte parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_TRUE:
+ return BooleanUtils.TRUE_BOOLEAN_AS_BYTE;
+ case VALUE_FALSE:
+ return BooleanUtils.FALSE_BOOLEAN_AS_BYTE;
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private byte parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new RepeaterGenericImpl<>(new ToBoolean(), null, null,
+ Type.booleanType().boxedType().arrayType());
+ }
+
+ final class ToBoolean implements ToObject {
+ @Override
+ public Boolean parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_TRUE:
+ return Boolean.TRUE;
+ case VALUE_FALSE:
+ return Boolean.FALSE;
+ case VALUE_NULL:
+ return parseFromNullBoolean(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromStringBoolean(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ @Override
+ public Boolean parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissingBoolean(parser);
+ }
+ }
+
+ private byte parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ if (!allowNull()) {
+ final byte res = Parsing.parseStringAsByteBool(parser, BooleanUtils.NULL_BOOLEAN_AS_BYTE);
+ if (res == BooleanUtils.NULL_BOOLEAN_AS_BYTE) {
+ throw nullNotAllowed(parser);
+ }
+ return res;
+ }
+ return Parsing.parseStringAsByteBool(parser, onNullByte);
+ }
+
+ private byte parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return onNullByte;
+ }
+
+ private byte parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return onMissingByte;
+ }
+
+ private Boolean parseFromStringBoolean(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ if (!allowNull()) {
+ final Boolean result = Parsing.parseStringAsBoolean(parser, null);
+ if (result == null) {
+ throw nullNotAllowed(parser);
+ }
+ return result;
+ }
+ return Parsing.parseStringAsBoolean(parser, onNull);
+ }
+
+ private Boolean parseFromNullBoolean(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return onNull;
+ }
+
+ private Boolean parseFromMissingBoolean(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return onMissing;
+ }
+
+ private class BoolMixinProcessor extends ValueProcessorMixinBase {
+ private WritableByteChunk> out;
+
+ @Override
+ public final void setContext(List> out) {
+ this.out = out.get(0).asWritableByteChunk();
+ }
+
+ @Override
+ public final void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteBufferInputStream.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteBufferInputStream.java
new file mode 100644
index 00000000000..17906ad0f84
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteBufferInputStream.java
@@ -0,0 +1,54 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+final class ByteBufferInputStream extends InputStream {
+
+ public static InputStream of(ByteBuffer buffer) {
+ if (buffer.hasArray()) {
+ return new ByteArrayInputStream(buffer.array(), buffer.arrayOffset() + buffer.position(),
+ buffer.remaining());
+ } else {
+ return new ByteBufferInputStream(buffer.asReadOnlyBuffer());
+ }
+ }
+
+ private final ByteBuffer buffer;
+
+ private ByteBufferInputStream(ByteBuffer buf) {
+ buffer = Objects.requireNonNull(buf);
+ }
+
+ @Override
+ public int available() {
+ return buffer.remaining();
+ }
+
+ @Override
+ public int read() {
+ return buffer.hasRemaining() ? buffer.get() & 0xFF : -1;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) {
+ if (!buffer.hasRemaining()) {
+ return -1;
+ }
+ len = Math.min(len, buffer.remaining());
+ buffer.get(bytes, off, len);
+ return len;
+ }
+
+ @Override
+ public long skip(long n) {
+ n = Math.min(n, buffer.remaining());
+ buffer.position(buffer.position() + (int) n);
+ return n;
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteMixin.java
new file mode 100644
index 00000000000..856491ab8cb
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ByteMixin.java
@@ -0,0 +1,151 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableByteChunk;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.sized.SizedByteChunk;
+import io.deephaven.json.ByteValue;
+import io.deephaven.json.Value;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class ByteMixin extends Mixin {
+ public ByteMixin(ByteValue options, JsonFactory factory) {
+ super(factory, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.byteType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new ByteMixinProcessor();
+ }
+
+ private byte parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ return parseFromInt(parser);
+ case VALUE_NUMBER_FLOAT:
+ return parseFromDecimal(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private byte parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new ByteRepeaterImpl();
+ }
+
+ final class ByteRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedByteChunk> chunk = new SizedByteChunk<>(0);
+
+ public ByteRepeaterImpl() {
+ super(null, null, Type.byteType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableByteChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, ByteMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableByteChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, ByteMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public byte[] doneImpl(JsonParser parser, int length) {
+ final WritableByteChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private byte parseFromInt(JsonParser parser) throws IOException {
+ checkNumberIntAllowed(parser);
+ return Parsing.parseIntAsByte(parser);
+ }
+
+ private byte parseFromDecimal(JsonParser parser) throws IOException {
+ checkDecimalAllowed(parser);
+ return Parsing.parseDecimalAsByte(parser);
+ }
+
+ private byte parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return allowDecimal()
+ ? Parsing.parseDecimalStringAsByte(parser)
+ : Parsing.parseStringAsByte(parser);
+ }
+
+ private byte parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_BYTE);
+ }
+
+ private byte parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_BYTE);
+ }
+
+ private class ByteMixinProcessor extends ValueProcessorMixinBase {
+ private WritableByteChunk> out;
+
+ @Override
+ public final void setContext(List> out) {
+ this.out = out.get(0).asWritableByteChunk();
+ }
+
+ @Override
+ public final void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/CharMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/CharMixin.java
new file mode 100644
index 00000000000..f14b81583f1
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/CharMixin.java
@@ -0,0 +1,134 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableCharChunk;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.sized.SizedCharChunk;
+import io.deephaven.json.CharValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class CharMixin extends Mixin {
+ public CharMixin(CharValue options, JsonFactory factory) {
+ super(factory, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.charType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new CharMixinProcessor();
+ }
+
+ private char parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private char parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new CharRepeaterImpl();
+ }
+
+ final class CharRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedCharChunk> chunk = new SizedCharChunk<>(0);
+
+ public CharRepeaterImpl() {
+ super(null, null, Type.charType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableCharChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, CharMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableCharChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, CharMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public char[] doneImpl(JsonParser parser, int length) {
+ final WritableCharChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private char parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return Parsing.parseStringAsChar(parser);
+ }
+
+ private char parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_CHAR);
+ }
+
+ private char parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_CHAR);
+ }
+
+ private class CharMixinProcessor extends ValueProcessorMixinBase {
+ private WritableCharChunk> out;
+
+ @Override
+ public void setContext(List> out) {
+ this.out = out.get(0).asWritableCharChunk();
+ }
+
+ @Override
+ public void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAware.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAware.java
new file mode 100644
index 00000000000..7e28d086bb4
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAware.java
@@ -0,0 +1,20 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.qst.type.Type;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+interface ContextAware {
+ void setContext(List> out);
+
+ void clearContext();
+
+ int numColumns();
+
+ Stream> columnTypes();
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAwareDelegateBase.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAwareDelegateBase.java
new file mode 100644
index 00000000000..bfe6a248727
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ContextAwareDelegateBase.java
@@ -0,0 +1,50 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.qst.type.Type;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+abstract class ContextAwareDelegateBase implements ContextAware {
+
+ private final Collection extends ContextAware> delegates;
+ private final int numColumns;
+
+ public ContextAwareDelegateBase(Collection extends ContextAware> delegates) {
+ this.delegates = Objects.requireNonNull(delegates);
+ this.numColumns = delegates.stream().mapToInt(ContextAware::numColumns).sum();
+ }
+
+ @Override
+ public final void setContext(List> out) {
+ int ix = 0;
+ for (ContextAware delegate : delegates) {
+ final int numColumns = delegate.numColumns();
+ delegate.setContext(out.subList(ix, ix + numColumns));
+ ix += numColumns;
+ }
+ }
+
+ @Override
+ public final void clearContext() {
+ for (ContextAware delegate : delegates) {
+ delegate.clearContext();
+ }
+ }
+
+ @Override
+ public final int numColumns() {
+ return numColumns;
+ }
+
+ @Override
+ public final Stream> columnTypes() {
+ return delegates.stream().flatMap(ContextAware::columnTypes);
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/DoubleMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/DoubleMixin.java
new file mode 100644
index 00000000000..bd1425a3784
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/DoubleMixin.java
@@ -0,0 +1,145 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableDoubleChunk;
+import io.deephaven.chunk.sized.SizedDoubleChunk;
+import io.deephaven.json.DoubleValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class DoubleMixin extends Mixin {
+
+ public DoubleMixin(DoubleValue options, JsonFactory factory) {
+ super(factory, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.doubleType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new DoubleMixinProcessor();
+ }
+
+ private double parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ case VALUE_NUMBER_FLOAT:
+ return parseFromNumber(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private double parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new DoubleRepeaterImpl();
+ }
+
+ final class DoubleRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedDoubleChunk> chunk = new SizedDoubleChunk<>(0);
+
+ public DoubleRepeaterImpl() {
+ super(null, null, Type.doubleType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableDoubleChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, DoubleMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableDoubleChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, DoubleMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public double[] doneImpl(JsonParser parser, int length) {
+ final WritableDoubleChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private double parseFromNumber(JsonParser parser) throws IOException {
+ checkNumberAllowed(parser);
+ // TODO: improve after https://github.com/FasterXML/jackson-core/issues/1229
+ return Parsing.parseNumberAsDouble(parser);
+ }
+
+ private double parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return Parsing.parseStringAsDouble(parser);
+ }
+
+ private double parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_DOUBLE);
+ }
+
+ private double parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_DOUBLE);
+ }
+
+ final class DoubleMixinProcessor extends ValueProcessorMixinBase {
+
+ private WritableDoubleChunk> out;
+
+ @Override
+ public void setContext(List> out) {
+ this.out = out.get(0).asWritableDoubleChunk();
+ }
+
+ @Override
+ public void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FieldProcessor.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FieldProcessor.java
new file mode 100644
index 00000000000..6a0b75e4a77
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FieldProcessor.java
@@ -0,0 +1,35 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import java.io.IOException;
+
+import static io.deephaven.json.jackson.Parsing.assertCurrentToken;
+
+interface FieldProcessor {
+
+ static void processFields(JsonParser parser, FieldProcessor fieldProcess) throws IOException {
+ while (parser.hasToken(JsonToken.FIELD_NAME)) {
+ final String fieldName = parser.currentName();
+ parser.nextToken();
+ fieldProcess.process(fieldName, parser);
+ parser.nextToken();
+ }
+ assertCurrentToken(parser, JsonToken.END_OBJECT);
+ }
+
+ static void skipFields(JsonParser parser) throws IOException {
+ while (parser.hasToken(JsonToken.FIELD_NAME)) {
+ parser.nextToken();
+ parser.skipChildren();
+ parser.nextToken();
+ }
+ assertCurrentToken(parser, JsonToken.END_OBJECT);
+ }
+
+ void process(String fieldName, JsonParser parser) throws IOException;
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FloatMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FloatMixin.java
new file mode 100644
index 00000000000..18ec8cde73a
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/FloatMixin.java
@@ -0,0 +1,144 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableFloatChunk;
+import io.deephaven.chunk.sized.SizedFloatChunk;
+import io.deephaven.json.FloatValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class FloatMixin extends Mixin {
+
+ public FloatMixin(FloatValue options, JsonFactory factory) {
+ super(factory, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.floatType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new FloatMixinProcessor();
+ }
+
+ private float parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ case VALUE_NUMBER_FLOAT:
+ return parseFromNumber(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private float parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new FloatRepeaterImpl();
+ }
+
+ final class FloatRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedFloatChunk> chunk = new SizedFloatChunk<>(0);
+
+ public FloatRepeaterImpl() {
+ super(null, null, Type.floatType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableFloatChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, FloatMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableFloatChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, FloatMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public float[] doneImpl(JsonParser parser, int length) {
+ final WritableFloatChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private float parseFromNumber(JsonParser parser) throws IOException {
+ checkNumberAllowed(parser);
+ return Parsing.parseNumberAsFloat(parser);
+ }
+
+ private float parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return Parsing.parseStringAsFloat(parser);
+ }
+
+ private float parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_FLOAT);
+ }
+
+ private float parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_FLOAT);
+ }
+
+ final class FloatMixinProcessor extends ValueProcessorMixinBase {
+
+ private WritableFloatChunk> out;
+
+ @Override
+ public void setContext(List> out) {
+ this.out = out.get(0).asWritableFloatChunk();
+ }
+
+ @Override
+ public void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/GenericObjectMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/GenericObjectMixin.java
new file mode 100644
index 00000000000..0a87f2f566a
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/GenericObjectMixin.java
@@ -0,0 +1,76 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableObjectChunk;
+import io.deephaven.json.Value;
+import io.deephaven.qst.type.GenericType;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+abstract class GenericObjectMixin extends Mixin implements ToObject {
+ private final GenericType type;
+
+ public GenericObjectMixin(JsonFactory factory, T options, GenericType type) {
+ super(factory, options);
+ this.type = Objects.requireNonNull(type);
+ }
+
+ @Override
+ public final int outputSize() {
+ return 1;
+ }
+
+ @Override
+ final Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ final Stream> outputTypesImpl() {
+ return Stream.of(type);
+ }
+
+ @Override
+ final ValueProcessor processor(String context) {
+ return new GenericObjectMixinProcessor();
+ }
+
+ @Override
+ final RepeaterProcessor repeaterProcessor() {
+ return new RepeaterGenericImpl<>(this, null, null, type.arrayType());
+ }
+
+ private class GenericObjectMixinProcessor extends ValueProcessorMixinBase {
+
+ private WritableObjectChunk out;
+
+ @Override
+ public final void setContext(List> out) {
+ this.out = out.get(0).asWritableObjectChunk();
+ }
+
+ @Override
+ public final void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantMixin.java
new file mode 100644
index 00000000000..7084213bbcf
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantMixin.java
@@ -0,0 +1,146 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableLongChunk;
+import io.deephaven.json.InstantValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.time.DateTimeUtils;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class InstantMixin extends Mixin {
+
+ private final long onNull;
+ private final long onMissing;
+
+ public InstantMixin(InstantValue options, JsonFactory factory) {
+ super(factory, options);
+ onNull = DateTimeUtils.epochNanos(options.onNull().orElse(null));
+ onMissing = DateTimeUtils.epochNanos(options.onMissing().orElse(null));
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.instantType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new InstantMixinProcessor();
+ }
+
+ private long parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private long parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new RepeaterGenericImpl<>(new ToObjectImpl(), null, null,
+ Type.instantType().arrayType());
+ }
+
+ class ToObjectImpl implements ToObject {
+ @Override
+ public Instant parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromStringToInstant(parser);
+ case VALUE_NULL:
+ return parseFromNullToInstant(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ @Override
+ public Instant parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissingToInstant(parser);
+ }
+ }
+
+ private long parseFromString(JsonParser parser) throws IOException {
+ final TemporalAccessor accessor = options.dateTimeFormatter().parse(Parsing.textAsCharSequence(parser));
+ final long epochSeconds = accessor.getLong(ChronoField.INSTANT_SECONDS);
+ final int nanoOfSecond = accessor.get(ChronoField.NANO_OF_SECOND);
+ return epochSeconds * 1_000_000_000L + nanoOfSecond;
+ }
+
+ private long parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return onNull;
+ }
+
+ private long parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return onMissing;
+ }
+
+ private Instant parseFromStringToInstant(JsonParser parser) throws IOException {
+ return Instant.from(options.dateTimeFormatter().parse(Parsing.textAsCharSequence(parser)));
+ }
+
+ private Instant parseFromNullToInstant(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(null);
+ }
+
+ private Instant parseFromMissingToInstant(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(null);
+ }
+
+ private class InstantMixinProcessor extends ValueProcessorMixinBase {
+ private WritableLongChunk> out;
+
+ @Override
+ public final void setContext(List> out) {
+ this.out = out.get(0).asWritableLongChunk();
+ }
+
+ @Override
+ public final void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantNumberMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantNumberMixin.java
new file mode 100644
index 00000000000..f8bd7befdb7
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/InstantNumberMixin.java
@@ -0,0 +1,178 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableLongChunk;
+import io.deephaven.json.InstantNumberValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.time.DateTimeUtils;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+final class InstantNumberMixin extends Mixin {
+
+ private final long onNull;
+ private final long onMissing;
+
+ public InstantNumberMixin(InstantNumberValue options, JsonFactory factory) {
+ super(factory, options);
+ onNull = DateTimeUtils.epochNanos(options.onNull().orElse(null));
+ onMissing = DateTimeUtils.epochNanos(options.onMissing().orElse(null));
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.instantType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new InstantNumberMixinProcessor(longFunction());
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new RepeaterGenericImpl<>(new ObjectImpl(), null, null,
+ Type.instantType().arrayType());
+ }
+
+ private LongImpl longFunction() {
+ switch (options.format()) {
+ case EPOCH_SECONDS:
+ return new LongImpl(9);
+ case EPOCH_MILLIS:
+ return new LongImpl(6);
+ case EPOCH_MICROS:
+ return new LongImpl(3);
+ case EPOCH_NANOS:
+ return new LongImpl(0);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private class LongImpl {
+
+ private final int scaled;
+ private final int mult;
+
+ LongImpl(int scaled) {
+ this.scaled = scaled;
+ this.mult = BigInteger.valueOf(10).pow(scaled).intValueExact();
+ }
+
+ private long parseFromInt(JsonParser parser) throws IOException {
+ return mult * Parsing.parseIntAsLong(parser);
+ }
+
+ private long parseFromDecimal(JsonParser parser) throws IOException {
+ // We need to parse w/ BigDecimal in the case of VALUE_NUMBER_FLOAT, otherwise we might lose accuracy
+ // jshell> (long)(1703292532.123456789 * 1000000000)
+ // $4 ==> 1703292532123456768
+ // See InstantNumberOptionsTest
+ return Parsing.parseDecimalAsScaledLong(parser, scaled);
+ }
+
+ private long parseFromString(JsonParser parser) throws IOException {
+ return mult * Parsing.parseStringAsLong(parser);
+ }
+
+ private long parseFromDecimalString(JsonParser parser) throws IOException {
+ return Parsing.parseDecimalStringAsScaledLong(parser, scaled);
+ }
+
+ public final long parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ checkNumberIntAllowed(parser);
+ return parseFromInt(parser);
+ case VALUE_NUMBER_FLOAT:
+ checkDecimalAllowed(parser);
+ return parseFromDecimal(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ checkStringAllowed(parser);
+ return allowDecimal()
+ ? parseFromDecimalString(parser)
+ : parseFromString(parser);
+ case VALUE_NULL:
+ checkNullAllowed(parser);
+ return onNull;
+ }
+ throw unexpectedToken(parser);
+ }
+
+ public final long parseMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return onMissing;
+ }
+ }
+
+ private class ObjectImpl implements ToObject {
+
+ private final LongImpl longImpl;
+
+ public ObjectImpl() {
+ this.longImpl = longFunction();
+ }
+
+ @Override
+ public Instant parseValue(JsonParser parser) throws IOException {
+ return DateTimeUtils.epochNanosToInstant(longImpl.parseValue(parser));
+ }
+
+ @Override
+ public Instant parseMissing(JsonParser parser) throws IOException {
+ return DateTimeUtils.epochNanosToInstant(longImpl.parseValue(parser));
+ }
+ }
+
+ private class InstantNumberMixinProcessor extends ValueProcessorMixinBase {
+ private final LongImpl impl;
+
+ private WritableLongChunk> out;
+
+ public InstantNumberMixinProcessor(LongImpl impl) {
+ this.impl = Objects.requireNonNull(impl);
+ }
+
+ @Override
+ public final void setContext(List> out) {
+ this.out = out.get(0).asWritableLongChunk();
+ }
+
+ @Override
+ public final void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(impl.parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(impl.parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/IntMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/IntMixin.java
new file mode 100644
index 00000000000..40cd67fcfc5
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/IntMixin.java
@@ -0,0 +1,152 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableIntChunk;
+import io.deephaven.chunk.sized.SizedIntChunk;
+import io.deephaven.json.IntValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class IntMixin extends Mixin {
+
+ public IntMixin(IntValue options, JsonFactory factory) {
+ super(factory, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.intType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new IntMixinProcessor();
+ }
+
+ private int parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ return parseFromInt(parser);
+ case VALUE_NUMBER_FLOAT:
+ return parseFromDecimal(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private int parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new IntRepeaterImpl();
+ }
+
+ final class IntRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedIntChunk> chunk = new SizedIntChunk<>(0);
+
+ public IntRepeaterImpl() {
+ super(null, null, Type.intType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableIntChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, IntMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableIntChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, IntMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public int[] doneImpl(JsonParser parser, int length) {
+ final WritableIntChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private int parseFromInt(JsonParser parser) throws IOException {
+ checkNumberIntAllowed(parser);
+ return Parsing.parseIntAsInt(parser);
+ }
+
+ private int parseFromDecimal(JsonParser parser) throws IOException {
+ checkDecimalAllowed(parser);
+ return Parsing.parseDecimalAsInt(parser);
+ }
+
+ private int parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return allowDecimal()
+ ? Parsing.parseDecimalStringAsInt(parser)
+ : Parsing.parseStringAsInt(parser);
+ }
+
+ private int parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_INT);
+ }
+
+ private int parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_INT);
+ }
+
+ final class IntMixinProcessor extends ValueProcessorMixinBase {
+
+ private WritableIntChunk> out;
+
+ @Override
+ public void setContext(List> out) {
+ this.out = out.get(0).asWritableIntChunk();
+ }
+
+ @Override
+ public void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonConfiguration.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonConfiguration.java
new file mode 100644
index 00000000000..c1de008ec88
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonConfiguration.java
@@ -0,0 +1,56 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonFactoryBuilder;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.core.StreamReadFeature;
+
+import java.lang.reflect.InvocationTargetException;
+
+public final class JacksonConfiguration {
+
+ private static final JsonFactory DEFAULT_FACTORY;
+
+ static {
+ // We'll attach an ObjectMapper if it's on the classpath, this allows parsing of AnyOptions
+ ObjectCodec objectCodec = null;
+ try {
+ final Class> clazz = Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
+ objectCodec = (ObjectCodec) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException
+ | InvocationTargetException e) {
+ // ignore
+ }
+ DEFAULT_FACTORY = defaultFactoryBuilder().build().setCodec(objectCodec);
+ }
+
+ /**
+ * Constructs a Deephaven-configured json factory builder. This currently includes
+ * {@link StreamReadFeature#USE_FAST_DOUBLE_PARSER}, {@link StreamReadFeature#USE_FAST_BIG_NUMBER_PARSER}, and
+ * {@link StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION}. The specific configuration may change in the future.
+ *
+ * @return the Deephaven-configured json factory builder
+ */
+ public static JsonFactoryBuilder defaultFactoryBuilder() {
+ return new JsonFactoryBuilder()
+ .enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER)
+ .enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER)
+ .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION);
+ }
+
+ // Not currently public, but javadoc still useful to ensure internal callers don't modify.
+ /**
+ * Returns a Deephaven-configured json factory singleton. Callers should not modify the returned factory in any way.
+ * This has been constructed as the singleton-equivalent of {@code defaultFactoryBuilder().build()}, with an
+ * ObjectMapper set as the codec if it is on the classpath.
+ *
+ * @return the Deephaven-configured json factory singleton
+ * @see #defaultFactoryBuilder()
+ */
+ static JsonFactory defaultFactory() {
+ return DEFAULT_FACTORY;
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonProvider.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonProvider.java
new file mode 100644
index 00000000000..e70cd0578ef
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonProvider.java
@@ -0,0 +1,291 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.TreeNode;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.json.AnyValue;
+import io.deephaven.json.ArrayValue;
+import io.deephaven.json.DoubleValue;
+import io.deephaven.json.IntValue;
+import io.deephaven.json.LongValue;
+import io.deephaven.json.ObjectEntriesValue;
+import io.deephaven.json.ObjectValue;
+import io.deephaven.json.StringValue;
+import io.deephaven.json.TupleValue;
+import io.deephaven.json.TypedObjectValue;
+import io.deephaven.json.Value;
+import io.deephaven.processor.NamedObjectProcessor;
+import io.deephaven.processor.ObjectProcessor;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.annotations.FinalDefault;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * A {@link Value JSON value} {@link ObjectProcessor processor} implementation using
+ * Jackson >.
+ *
+ *
+ * This implementation allows users to efficiently parse / destructure a
+ * JSON value (from a supported {@link JacksonProvider#getInputTypes()
+ * input type}) into {@link WritableChunk writable chunks} according to the type(s) as specified by its {@link Value
+ * Value type}. This is done using the Jackson streaming
+ * API {@link JsonParser} (as opposed to the databind / object mapping API, which must first create intermediate
+ * objects).
+ *
+ *
+ * The "simple" types are self-explanatory. For example, the {@link StringValue} represents a {@link String} output
+ * type, and (by default) expects a JSON string as input; the {@link IntValue} represents an {@code int} output type,
+ * and (by default) expects a JSON number as input. The allowed JSON input types can be specified via
+ * {@link Value#allowedTypes() allowed types}; users are encouraged to use the strictest type they can according to how
+ * their JSON data is serialized.
+ *
+ *
+ * The most common "complex" type is {@link ObjectValue}, which expects to parse a JSON object of known fields. The
+ * object contains {@link ObjectValue#fields()}, which represent other {@link Value values}. The fields are recursively
+ * resolved and flattened into the {@link ObjectProcessor#outputTypes()}. For example, a JSON object, which itself
+ * contains another JSON object
+ *
+ *
+ * {
+ * "city": "Plymouth",
+ * "point": {
+ * "latitude": 45.018269,
+ * "longitude": -93.473892
+ * }
+ * }
+ *
+ *
+ * when represented with structuring as one might expect ({@link ObjectValue}({@link StringValue},
+ * {@link ObjectValue}({@link DoubleValue}, {@link DoubleValue}))), will produce {@link ObjectProcessor#outputTypes()
+ * output types} representing {@code [String, double, double]}. Furthermore, the field names and delimiter "_" will be
+ * used by default to provide the {@link NamedObjectProcessor#names() names}
+ * {@code ["city", "point_latitude", "point_longitude"]}.
+ *
+ *
+ * The {@link ArrayValue} represents a variable-length array, which expects to parse a JSON array where each element is
+ * expected to have the same {@link ArrayValue#element() element type}. (This is in contrast to JSON arrays more
+ * generally, where each element of the array can be a different JSON value type.) The output type will be the output
+ * type(s) of the element type as the component type of a native array, {@link Type#arrayType()}. For example, if we
+ * used the previous example as the {@link ArrayValue#element() array component type}, it will produce
+ * {@link ObjectProcessor#outputTypes() output types} representing {@code [String[], double[], double[]]} (the
+ * {@link NamedObjectProcessor#names() names} will remain unchanged).
+ *
+ *
+ * The {@link TupleValue} represents a fixed number of {@link TupleValue#namedValues() value types}, which expects to
+ * parse a fixed-length JSON array where each element corresponds to the same-indexed value type. The values are
+ * recursively resolved and flattened into the {@link ObjectProcessor#outputTypes()}; for example, the earlier example's
+ * data could be re-represented as the JSON array
+ *
+ *
+ * ["Plymouth", 45.018269, -93.473892]
+ *
+ *
+ * and structured as one might expect ({@link TupleValue}({@link StringValue}, {@link DoubleValue},
+ * {@link DoubleValue})), and will produce {@link ObjectProcessor#outputTypes() output types} representing
+ * {@code [String, double, double]}. Even though no field names are present in the JSON value, users may set
+ * {@link TupleValue#namedValues() names} for each element (and will otherwise inherit integer-indexed default names).
+ *
+ *
+ * The {@link TypedObjectValue} represents a union of {@link ObjectValue object values} where the first field is
+ * type-discriminating. For example, the following might be modelled as a type-discriminated object with
+ * type-discriminating field "type", shared "symbol" {@link StringValue}, "quote" object of "bid" {@link DoubleValue}
+ * and an "ask" {@link DoubleValue}, and "trade" object containing a "price" {@link DoubleValue} and a "size"
+ * {@link LongValue}.
+ *
+ *
+ * {
+ * "type": "quote",
+ * "symbol": "BAR",
+ * "bid": 10.01,
+ * "ask": 10.05
+ * }
+ * {
+ * "type": "trade",
+ * "symbol": "FOO",
+ * "price": 70.03,
+ * "size": 42
+ * }
+ *
+ *
+ * The {@link ObjectProcessor#outputTypes() output types} are first the type-discriminating field, then the shared
+ * fields (if any), followed by the individual {@link ObjectValue object value} fields; with the above example, that
+ * would result in {@link ObjectProcessor#outputTypes() output types}
+ * {@code [String, String, double, double, double long]} and {@link NamedObjectProcessor#names() names}
+ * {@code ["type", "symbol", "quote_bid", "quote_ask", "trade_price", "trade_size"]}.
+ *
+ *
+ * The {@link ObjectEntriesValue} represents a variable-length object, which expects to parse a JSON object where each
+ * key-value entry has a common {@link ObjectEntriesValue#value() value type}. The output type will be the key and value
+ * element types as a component of native arrays ({@link Type#arrayType()}). For example, a JSON object, whose values
+ * are also JSON objects
+ *
+ *
+ * {
+ * "Plymouth": {
+ * "latitude": 45.018269,
+ * "longitude": -93.473892
+ * },
+ * "New York": {
+ * "latitude": 40.730610,
+ * "longitude": -73.935242
+ * }
+ * }
+ *
+ *
+ * when represented with structuring as one might expect ({@link ObjectEntriesValue}({@link StringValue},
+ * {@link ObjectValue}({@link DoubleValue}, {@link DoubleValue}))), will produce {@link ObjectProcessor#outputTypes()
+ * output types} representing {@code [String[], double[], double[]]}, and {@link NamedObjectProcessor#names() names}
+ * {@code ["Key", "latitude", "longitude"]}.
+ *
+ *
+ * The {@link AnyValue} type represents a {@link TreeNode} output; this requires that the Jackson databinding API be
+ * available on the classpath. This is useful for initial modelling and debugging purposes.
+ */
+public interface JacksonProvider extends NamedObjectProcessor.Provider {
+
+ /**
+ * Creates a jackson provider using a default factory. Equivalent to
+ * {@code of(options, JacksonConfiguration.defaultFactoryBuilder().build())}.
+ *
+ * @param options the object options
+ * @return the jackson provider
+ * @see #of(Value, JsonFactory)
+ * @see JacksonConfiguration#defaultFactoryBuilder()
+ */
+ static JacksonProvider of(Value options) {
+ return of(options, JacksonConfiguration.defaultFactory());
+ }
+
+ /**
+ * Creates a jackson provider using the provided {@code factory}.
+ *
+ * @param options the object options
+ * @param factory the jackson factory
+ * @return the jackson provider
+ */
+ static JacksonProvider of(Value options, JsonFactory factory) {
+ return Mixin.of(options, factory);
+ }
+
+ /**
+ * The supported types. Includes {@link String}, {@code byte[]}, {@code char[]}, {@link File}, {@link Path},
+ * {@link URL}, {@link ByteBuffer}, and {@link CharBuffer}.
+ *
+ * @return the supported types
+ */
+ static Set> getInputTypes() {
+ return Set.of(
+ Type.stringType(),
+ Type.byteType().arrayType(),
+ Type.charType().arrayType(),
+ Type.ofCustom(File.class),
+ Type.ofCustom(Path.class),
+ Type.ofCustom(URL.class),
+ Type.ofCustom(ByteBuffer.class),
+ Type.ofCustom(CharBuffer.class));
+ }
+
+ /**
+ * The supported types. Equivalent to {@link #getInputTypes()}.
+ *
+ * @return the supported types
+ */
+ @Override
+ @FinalDefault
+ default Set> inputTypes() {
+ return getInputTypes();
+ }
+
+ /**
+ * Creates an object processor based on the {@code inputType} with a default {@link JsonFactory}.
+ *
+ * @param inputType the input type
+ * @return the object processor
+ * @param the input type
+ * @see #stringProcessor()
+ * @see #bytesProcessor()
+ * @see #charsProcessor()
+ * @see #fileProcessor()
+ * @see #pathProcessor()
+ * @see #urlProcessor()
+ * @see #byteBufferProcessor()
+ * @see #charBufferProcessor()
+ */
+ @Override
+ ObjectProcessor super T> processor(Type inputType);
+
+ List names(Function, String> f);
+
+ /**
+ * Creates a {@link String} json object processor.
+ *
+ * @return the object processor
+ * @see JsonFactory#createParser(String)
+ */
+ ObjectProcessor stringProcessor();
+
+ /**
+ * Creates a {@code byte[]} json object processor.
+ *
+ * @return the object processor
+ * @see JsonFactory#createParser(byte[])
+ */
+ ObjectProcessor bytesProcessor();
+
+ /**
+ * Creates a {@code char[]} json object processor.
+ *
+ * @return the object processor
+ * @see JsonFactory#createParser(char[])
+ */
+ ObjectProcessor charsProcessor();
+
+ /**
+ * Creates a {@link File} json object processor.
+ *
+ * @return the object processor
+ * @see JsonFactory#createParser(File)
+ */
+ ObjectProcessor fileProcessor();
+
+ /**
+ * Creates a {@link Path} json object processor.
+ *
+ * @return the object processor
+ */
+ ObjectProcessor pathProcessor();
+
+ /**
+ * Creates a {@link URL} json object processor.
+ *
+ * @return the object processor
+ * @see JsonFactory#createParser(URL)
+ */
+ ObjectProcessor urlProcessor();
+
+ /**
+ * Creates a {@link ByteBuffer} json object processor.
+ *
+ * @return the object processor
+ */
+ ObjectProcessor byteBufferProcessor();
+
+ /**
+ * Creates a {@link CharBuffer} json object processor.
+ *
+ * @return the object processor
+ */
+ ObjectProcessor charBufferProcessor();
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonSource.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonSource.java
new file mode 100644
index 00000000000..adb9b278ce0
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/JacksonSource.java
@@ -0,0 +1,72 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.StreamReadFeature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+final class JacksonSource {
+
+ public static JsonParser of(JsonFactory factory, String content) throws IOException {
+ return factory.createParser(content);
+ }
+
+ public static JsonParser of(JsonFactory factory, File file) throws IOException {
+ return factory.createParser(file);
+ }
+
+ public static JsonParser of(JsonFactory factory, Path path) throws IOException {
+ if (FileSystems.getDefault() == path.getFileSystem()) {
+ return of(factory, path.toFile());
+ }
+ if (!factory.isEnabled(StreamReadFeature.AUTO_CLOSE_SOURCE)) {
+ throw new RuntimeException(String.format("Unable to create Path-based parser when '%s' is not enabled",
+ StreamReadFeature.AUTO_CLOSE_SOURCE));
+ }
+ // jackson buffers internally
+ return factory.createParser(Files.newInputStream(path));
+ }
+
+ public static JsonParser of(JsonFactory factory, InputStream inputStream) throws IOException {
+ return factory.createParser(inputStream);
+ }
+
+ public static JsonParser of(JsonFactory factory, URL url) throws IOException {
+ return factory.createParser(url);
+ }
+
+ public static JsonParser of(JsonFactory factory, byte[] array, int offset, int len) throws IOException {
+ return factory.createParser(array, offset, len);
+ }
+
+ public static JsonParser of(JsonFactory factory, ByteBuffer buffer) throws IOException {
+ if (buffer.hasArray()) {
+ return of(factory, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ }
+ return of(factory, ByteBufferInputStream.of(buffer));
+ }
+
+ public static JsonParser of(JsonFactory factory, char[] array, int offset, int len) throws IOException {
+ return factory.createParser(array, offset, len);
+ }
+
+ public static JsonParser of(JsonFactory factory, CharBuffer buffer) throws IOException {
+ if (buffer.hasArray()) {
+ return of(factory, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
+ }
+ // We could be more efficient here with CharBufferReader. Surprised it's not build into JDK.
+ return of(factory, buffer.toString());
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LocalDateMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LocalDateMixin.java
new file mode 100644
index 00000000000..2ad69c30932
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LocalDateMixin.java
@@ -0,0 +1,52 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.json.LocalDateValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.temporal.TemporalAccessor;
+
+final class LocalDateMixin extends GenericObjectMixin {
+
+ public LocalDateMixin(LocalDateValue options, JsonFactory factory) {
+ super(factory, options, Type.ofCustom(LocalDate.class));
+ }
+
+ @Override
+ public LocalDate parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ @Override
+ public LocalDate parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ private LocalDate parseFromString(JsonParser parser) throws IOException {
+ final TemporalAccessor accessor = options.dateTimeFormatter().parse(Parsing.textAsCharSequence(parser));
+ return LocalDate.from(accessor);
+ }
+
+ private LocalDate parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(null);
+ }
+
+ private LocalDate parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(null);
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LongMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LongMixin.java
new file mode 100644
index 00000000000..c7a69e7f47f
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/LongMixin.java
@@ -0,0 +1,151 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.base.MathUtil;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.chunk.WritableLongChunk;
+import io.deephaven.chunk.sized.SizedLongChunk;
+import io.deephaven.json.LongValue;
+import io.deephaven.qst.type.Type;
+import io.deephaven.util.QueryConstants;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class LongMixin extends Mixin {
+
+ public LongMixin(LongValue options, JsonFactory config) {
+ super(config, options);
+ }
+
+ @Override
+ public int outputSize() {
+ return 1;
+ }
+
+ @Override
+ public Stream> paths() {
+ return Stream.of(List.of());
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.of(Type.longType());
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new LongMixinProcessor();
+ }
+
+ private long parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case VALUE_NUMBER_INT:
+ return parseFromInt(parser);
+ case VALUE_NUMBER_FLOAT:
+ return parseFromDecimal(parser);
+ case VALUE_STRING:
+ case FIELD_NAME:
+ return parseFromString(parser);
+ case VALUE_NULL:
+ return parseFromNull(parser);
+ }
+ throw unexpectedToken(parser);
+ }
+
+ private long parseMissing(JsonParser parser) throws IOException {
+ return parseFromMissing(parser);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new LongRepeaterImpl();
+ }
+
+ final class LongRepeaterImpl extends RepeaterProcessorBase {
+ private final SizedLongChunk> chunk = new SizedLongChunk<>(0);
+
+ public LongRepeaterImpl() {
+ super(null, null, Type.longType().arrayType());
+ }
+
+ @Override
+ public void processElementImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableLongChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, LongMixin.this.parseValue(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public void processElementMissingImpl(JsonParser parser, int index) throws IOException {
+ final int newSize = index + 1;
+ final WritableLongChunk> chunk = this.chunk.ensureCapacityPreserve(MathUtil.roundUpArraySize(newSize));
+ chunk.set(index, LongMixin.this.parseMissing(parser));
+ chunk.setSize(newSize);
+ }
+
+ @Override
+ public long[] doneImpl(JsonParser parser, int length) {
+ final WritableLongChunk> chunk = this.chunk.get();
+ return Arrays.copyOfRange(chunk.array(), chunk.arrayOffset(), chunk.arrayOffset() + length);
+ }
+ }
+
+ private long parseFromInt(JsonParser parser) throws IOException {
+ checkNumberIntAllowed(parser);
+ return Parsing.parseIntAsLong(parser);
+ }
+
+ private long parseFromDecimal(JsonParser parser) throws IOException {
+ checkDecimalAllowed(parser);
+ return Parsing.parseDecimalAsLong(parser);
+ }
+
+ private long parseFromString(JsonParser parser) throws IOException {
+ checkStringAllowed(parser);
+ return allowDecimal()
+ ? Parsing.parseDecimalStringAsLong(parser)
+ : Parsing.parseStringAsLong(parser);
+ }
+
+ private long parseFromNull(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ return options.onNull().orElse(QueryConstants.NULL_LONG);
+ }
+
+ private long parseFromMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ return options.onMissing().orElse(QueryConstants.NULL_LONG);
+ }
+
+ private class LongMixinProcessor extends ValueProcessorMixinBase {
+ private WritableLongChunk> out;
+
+ @Override
+ public void setContext(List> out) {
+ this.out = out.get(0).asWritableLongChunk();
+ }
+
+ @Override
+ public void clearContext() {
+ out = null;
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ out.add(parseValue(parser));
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ out.add(parseMissing(parser));
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/Mixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/Mixin.java
new file mode 100644
index 00000000000..e0b6c89f640
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/Mixin.java
@@ -0,0 +1,521 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.api.util.NameValidator;
+import io.deephaven.json.AnyValue;
+import io.deephaven.json.ArrayValue;
+import io.deephaven.json.BigDecimalValue;
+import io.deephaven.json.BigIntegerValue;
+import io.deephaven.json.BoolValue;
+import io.deephaven.json.ByteValue;
+import io.deephaven.json.CharValue;
+import io.deephaven.json.DoubleValue;
+import io.deephaven.json.FloatValue;
+import io.deephaven.json.InstantNumberValue;
+import io.deephaven.json.InstantValue;
+import io.deephaven.json.IntValue;
+import io.deephaven.json.JsonValueTypes;
+import io.deephaven.json.LocalDateValue;
+import io.deephaven.json.LongValue;
+import io.deephaven.json.ObjectField;
+import io.deephaven.json.ObjectEntriesValue;
+import io.deephaven.json.ObjectValue;
+import io.deephaven.json.ShortValue;
+import io.deephaven.json.SkipValue;
+import io.deephaven.json.StringValue;
+import io.deephaven.json.TupleValue;
+import io.deephaven.json.TypedObjectValue;
+import io.deephaven.json.Value;
+import io.deephaven.processor.ObjectProcessor;
+import io.deephaven.qst.type.Type;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+abstract class Mixin implements JacksonProvider {
+
+ static final Function, String> TO_COLUMN_NAME = Mixin::toColumnName;
+
+ public static String toColumnName(List path) {
+ return path.isEmpty() ? "Value" : String.join("_", path);
+ }
+
+ static Mixin> of(Value options, JsonFactory factory) {
+ return options.walk(new MixinImpl(factory));
+ }
+
+ private final JsonFactory factory;
+ final T options;
+
+ Mixin(JsonFactory factory, T options) {
+ this.factory = Objects.requireNonNull(factory);
+ this.options = Objects.requireNonNull(options);
+ }
+
+ @Override
+ public final List> outputTypes() {
+ return outputTypesImpl().collect(Collectors.toList());
+ }
+
+ @Override
+ public final List names() {
+ return names(TO_COLUMN_NAME);
+ }
+
+ @Override
+ public final List names(Function, String> f) {
+ return Arrays.asList(NameValidator.legalizeColumnNames(paths().map(f).toArray(String[]::new), true));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final ObjectProcessor super X> processor(Type inputType) {
+ final Class clazz = inputType.clazz();
+ if (String.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) stringProcessor();
+ }
+ if (byte[].class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) bytesProcessor();
+ }
+ if (char[].class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) charsProcessor();
+ }
+ if (File.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) fileProcessor();
+ }
+ if (Path.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) pathProcessor();
+ }
+ if (URL.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) urlProcessor();
+ }
+ if (ByteBuffer.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) byteBufferProcessor();
+ }
+ if (CharBuffer.class.isAssignableFrom(clazz)) {
+ return (ObjectProcessor super X>) charBufferProcessor();
+ }
+ throw new IllegalArgumentException("Unable to create JSON processor from type " + inputType);
+ }
+
+ @Override
+ public final ObjectProcessor stringProcessor() {
+ return new StringIn();
+ }
+
+ @Override
+ public final ObjectProcessor bytesProcessor() {
+ return new BytesIn();
+ }
+
+ @Override
+ public final ObjectProcessor charsProcessor() {
+ return new CharsIn();
+ }
+
+ @Override
+ public final ObjectProcessor fileProcessor() {
+ return new FileIn();
+ }
+
+ @Override
+ public final ObjectProcessor pathProcessor() {
+ return new PathIn();
+ }
+
+ @Override
+ public final ObjectProcessor urlProcessor() {
+ return new URLIn();
+ }
+
+ @Override
+ public final ObjectProcessor byteBufferProcessor() {
+ return new ByteBufferIn();
+ }
+
+ @Override
+ public final ObjectProcessor charBufferProcessor() {
+ return new CharBufferIn();
+ }
+
+ abstract ValueProcessor processor(String context);
+
+ abstract RepeaterProcessor repeaterProcessor();
+
+ abstract Stream> paths();
+
+ abstract Stream> outputTypesImpl();
+
+ static List prefixWith(String prefix, List path) {
+ return Stream.concat(Stream.of(prefix), path.stream()).collect(Collectors.toList());
+ }
+
+ static Stream> prefixWithKeys(Map> fields) {
+ final List>> paths = new ArrayList<>(fields.size());
+ for (Entry> e : fields.entrySet()) {
+ final Stream> prefixedPaths = e.getValue().paths().map(x -> prefixWith(e.getKey().name(), x));
+ paths.add(prefixedPaths);
+ }
+ return paths.stream().flatMap(Function.identity());
+ }
+
+ static Stream> prefixWithKeysAndSkip(Map> fields, int skip) {
+ final List>> paths = new ArrayList<>(fields.size());
+ for (Entry> e : fields.entrySet()) {
+ final Stream> prefixedPaths =
+ e.getValue().paths().map(x -> prefixWith(e.getKey(), x)).skip(skip);
+ paths.add(prefixedPaths);
+ }
+ return paths.stream().flatMap(Function.identity());
+ }
+
+ private abstract class ObjectProcessorMixin extends ObjectProcessorJsonValue {
+ public ObjectProcessorMixin() {
+ super(Mixin.this.processor(""));
+ }
+ }
+
+ private class StringIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(String in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private class BytesIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(byte[] in) throws IOException {
+ return JacksonSource.of(factory, in, 0, in.length);
+ }
+ }
+
+ private class ByteBufferIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(ByteBuffer in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private class CharBufferIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(CharBuffer in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private class CharsIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(char[] in) throws IOException {
+ return JacksonSource.of(factory, in, 0, in.length);
+ }
+ }
+
+ private class FileIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(File in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private class PathIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(Path in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private class URLIn extends ObjectProcessorMixin {
+ @Override
+ protected JsonParser createParser(URL in) throws IOException {
+ return JacksonSource.of(factory, in);
+ }
+ }
+
+ private static class MixinImpl implements Value.Visitor> {
+ private final JsonFactory factory;
+
+ public MixinImpl(JsonFactory factory) {
+ this.factory = Objects.requireNonNull(factory);
+ }
+
+ @Override
+ public StringMixin visit(StringValue _string) {
+ return new StringMixin(_string, factory);
+ }
+
+ @Override
+ public Mixin> visit(BoolValue _bool) {
+ return new BoolMixin(_bool, factory);
+ }
+
+ @Override
+ public Mixin> visit(ByteValue _byte) {
+ return new ByteMixin(_byte, factory);
+ }
+
+ @Override
+ public Mixin> visit(CharValue _char) {
+ return new CharMixin(_char, factory);
+ }
+
+ @Override
+ public Mixin> visit(ShortValue _short) {
+ return new ShortMixin(_short, factory);
+ }
+
+ @Override
+ public IntMixin visit(IntValue _int) {
+ return new IntMixin(_int, factory);
+ }
+
+ @Override
+ public LongMixin visit(LongValue _long) {
+ return new LongMixin(_long, factory);
+ }
+
+ @Override
+ public FloatMixin visit(FloatValue _float) {
+ return new FloatMixin(_float, factory);
+ }
+
+ @Override
+ public DoubleMixin visit(DoubleValue _double) {
+ return new DoubleMixin(_double, factory);
+ }
+
+ @Override
+ public ObjectMixin visit(ObjectValue object) {
+ return new ObjectMixin(object, factory);
+ }
+
+ @Override
+ public Mixin> visit(ObjectEntriesValue objectKv) {
+ return new ObjectEntriesMixin(objectKv, factory);
+ }
+
+ @Override
+ public InstantMixin visit(InstantValue instant) {
+ return new InstantMixin(instant, factory);
+ }
+
+ @Override
+ public InstantNumberMixin visit(InstantNumberValue instantNumber) {
+ return new InstantNumberMixin(instantNumber, factory);
+ }
+
+ @Override
+ public BigIntegerMixin visit(BigIntegerValue bigInteger) {
+ return new BigIntegerMixin(bigInteger, factory);
+ }
+
+ @Override
+ public BigDecimalMixin visit(BigDecimalValue bigDecimal) {
+ return new BigDecimalMixin(bigDecimal, factory);
+ }
+
+ @Override
+ public SkipMixin visit(SkipValue skip) {
+ return new SkipMixin(skip, factory);
+ }
+
+ @Override
+ public TupleMixin visit(TupleValue tuple) {
+ return new TupleMixin(tuple, factory);
+ }
+
+ @Override
+ public TypedObjectMixin visit(TypedObjectValue typedObject) {
+ return new TypedObjectMixin(typedObject, factory);
+ }
+
+ @Override
+ public LocalDateMixin visit(LocalDateValue localDate) {
+ return new LocalDateMixin(localDate, factory);
+ }
+
+ @Override
+ public ArrayMixin visit(ArrayValue array) {
+ return new ArrayMixin(array, factory);
+ }
+
+ @Override
+ public AnyMixin visit(AnyValue any) {
+ return new AnyMixin(any, factory);
+ }
+ }
+
+ final boolean allowNull() {
+ return options.allowedTypes().contains(JsonValueTypes.NULL);
+ }
+
+ final boolean allowMissing() {
+ return options.allowMissing();
+ }
+
+ final boolean allowNumberInt() {
+ return options.allowedTypes().contains(JsonValueTypes.INT);
+ }
+
+ final boolean allowDecimal() {
+ return options.allowedTypes().contains(JsonValueTypes.DECIMAL);
+ }
+
+ final void checkNumberAllowed(JsonParser parser) throws IOException {
+ if (!allowNumberInt() && !allowDecimal()) {
+ throw new ValueAwareException("Number not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkNumberIntAllowed(JsonParser parser) throws IOException {
+ if (!allowNumberInt()) {
+ throw new ValueAwareException("Number int not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkDecimalAllowed(JsonParser parser) throws IOException {
+ if (!allowDecimal()) {
+ throw new ValueAwareException("Decimal not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkBoolAllowed(JsonParser parser) throws IOException {
+ if (!options.allowedTypes().contains(JsonValueTypes.BOOL)) {
+ throw new ValueAwareException("Bool not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkStringAllowed(JsonParser parser) throws IOException {
+ if (!options.allowedTypes().contains(JsonValueTypes.STRING)) {
+ throw new ValueAwareException("String not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkObjectAllowed(JsonParser parser) throws IOException {
+ if (!options.allowedTypes().contains(JsonValueTypes.OBJECT)) {
+ throw new ValueAwareException("Object not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkArrayAllowed(JsonParser parser) throws IOException {
+ if (!options.allowedTypes().contains(JsonValueTypes.ARRAY)) {
+ throw new ValueAwareException("Array not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final void checkNullAllowed(JsonParser parser) throws IOException {
+ if (!allowNull()) {
+ throw nullNotAllowed(parser);
+ }
+ }
+
+ final ValueAwareException nullNotAllowed(JsonParser parser) {
+ return new ValueAwareException("Null not allowed", parser.currentLocation(), options);
+ }
+
+ final void checkMissingAllowed(JsonParser parser) throws IOException {
+ if (!allowMissing()) {
+ throw new ValueAwareException("Missing not allowed", parser.currentLocation(), options);
+ }
+ }
+
+ final IOException unexpectedToken(JsonParser parser) throws ValueAwareException {
+ final String msg;
+ switch (parser.currentToken()) {
+ case VALUE_TRUE:
+ case VALUE_FALSE:
+ msg = "Bool not expected";
+ break;
+ case START_OBJECT:
+ msg = "Object not expected";
+ break;
+ case START_ARRAY:
+ msg = "Array not expected";
+ break;
+ case VALUE_NUMBER_INT:
+ msg = "Number int not expected";
+ break;
+ case VALUE_NUMBER_FLOAT:
+ msg = "Decimal not expected";
+ break;
+ case FIELD_NAME:
+ msg = "Field name not expected";
+ break;
+ case VALUE_STRING:
+ msg = "String not expected";
+ break;
+ case VALUE_NULL:
+ msg = "Null not expected";
+ break;
+ default:
+ msg = parser.currentToken() + " not expected";
+ }
+ throw new ValueAwareException(msg, parser.currentLocation(), options);
+ }
+
+ abstract class ValueProcessorMixinBase implements ValueProcessor {
+ @Override
+ public final int numColumns() {
+ return Mixin.this.outputSize();
+ }
+
+ @Override
+ public final Stream> columnTypes() {
+ return Mixin.this.outputTypesImpl();
+ }
+
+ @Override
+ public final void processCurrentValue(JsonParser parser) throws IOException {
+ try {
+ processCurrentValueImpl(parser);
+ } catch (ValueAwareException e) {
+ if (options.equals(e.value())) {
+ throw e;
+ } else {
+ throw wrap(parser, e, "Unable to process current value");
+ }
+ } catch (IOException e) {
+ throw wrap(parser, e, "Unable to process current value");
+ }
+ }
+
+ @Override
+ public final void processMissing(JsonParser parser) throws IOException {
+ try {
+ processMissingImpl(parser);
+ } catch (ValueAwareException e) {
+ if (options.equals(e.value())) {
+ throw e;
+ } else {
+ throw wrap(parser, e, "Unable to process missing value");
+ }
+ } catch (IOException e) {
+ throw wrap(parser, e, "Unable to process missing value");
+ }
+ }
+
+ protected abstract void processCurrentValueImpl(JsonParser parser) throws IOException;
+
+ protected abstract void processMissingImpl(JsonParser parser) throws IOException;
+
+ private ValueAwareException wrap(JsonParser parser, IOException e, String msg) {
+ return new ValueAwareException(msg, parser.currentLocation(), e, options);
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectEntriesMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectEntriesMixin.java
new file mode 100644
index 00000000000..75c3267168d
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectEntriesMixin.java
@@ -0,0 +1,105 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import io.deephaven.chunk.WritableChunk;
+import io.deephaven.json.ObjectEntriesValue;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class ObjectEntriesMixin extends Mixin {
+ private final Mixin> key;
+ private final Mixin> value;
+
+ public ObjectEntriesMixin(ObjectEntriesValue options, JsonFactory factory) {
+ super(factory, options);
+ key = Mixin.of(options.key(), factory);
+ value = Mixin.of(options.value(), factory);
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return Stream.concat(key.outputTypesImpl(), value.outputTypesImpl()).map(Type::arrayType);
+ }
+
+ @Override
+ public int outputSize() {
+ return key.outputSize() + value.outputSize();
+ }
+
+ @Override
+ public Stream> paths() {
+ final Stream> keyPath =
+ key.outputSize() == 1 && key.paths().findFirst().orElseThrow().isEmpty()
+ ? Stream.of(List.of("Key"))
+ : key.paths();
+ final Stream> valuePath =
+ value.outputSize() == 1 && value.paths().findFirst().orElseThrow().isEmpty()
+ ? Stream.of(List.of("Value"))
+ : value.paths();
+ return Stream.concat(keyPath, valuePath);
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return new ObjectEntriesMixinProcessor();
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ return new ValueInnerRepeaterProcessor(new ObjectEntriesMixinProcessor());
+ }
+
+ private class ObjectEntriesMixinProcessor extends ValueProcessorMixinBase {
+
+ private final RepeaterProcessor keyProcessor;
+ private final RepeaterProcessor valueProcessor;
+
+ ObjectEntriesMixinProcessor() {
+ this.keyProcessor = key.repeaterProcessor();
+ this.valueProcessor = value.repeaterProcessor();
+ }
+
+ @Override
+ public void setContext(List> out) {
+ final int keySize = keyProcessor.numColumns();
+ keyProcessor.setContext(out.subList(0, keySize));
+ valueProcessor.setContext(out.subList(keySize, keySize + valueProcessor.numColumns()));
+ }
+
+ @Override
+ public void clearContext() {
+ keyProcessor.clearContext();
+ valueProcessor.clearContext();
+ }
+
+ @Override
+ protected void processCurrentValueImpl(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case START_OBJECT:
+ RepeaterProcessor.processObjectKeyValues(parser, keyProcessor, valueProcessor);
+ return;
+ case VALUE_NULL:
+ checkNullAllowed(parser);
+ keyProcessor.processNullRepeater(parser);
+ valueProcessor.processNullRepeater(parser);
+ return;
+ default:
+ throw unexpectedToken(parser);
+ }
+ }
+
+ @Override
+ protected void processMissingImpl(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ keyProcessor.processMissingRepeater(parser);
+ valueProcessor.processMissingRepeater(parser);
+ }
+ }
+}
diff --git a/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectMixin.java b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectMixin.java
new file mode 100644
index 00000000000..ea14baf40bf
--- /dev/null
+++ b/extensions/json-jackson/src/main/java/io/deephaven/json/jackson/ObjectMixin.java
@@ -0,0 +1,448 @@
+//
+// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
+//
+package io.deephaven.json.jackson;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import io.deephaven.json.ObjectField;
+import io.deephaven.json.ObjectField.RepeatedBehavior;
+import io.deephaven.json.ObjectValue;
+import io.deephaven.json.jackson.RepeaterProcessor.Context;
+import io.deephaven.qst.type.Type;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+
+final class ObjectMixin extends Mixin {
+
+ private final Map> mixins;
+ private final int numOutputs;
+
+ public ObjectMixin(ObjectValue options, JsonFactory factory) {
+ super(factory, options);
+ final LinkedHashMap> map = new LinkedHashMap<>(options.fields().size());
+ for (ObjectField field : options.fields()) {
+ map.put(field, Mixin.of(field.options(), factory));
+ }
+ mixins = Collections.unmodifiableMap(map);
+ numOutputs = mixins.values().stream().mapToInt(Mixin::outputSize).sum();
+ }
+
+ @Override
+ public Stream> outputTypesImpl() {
+ return mixins.values().stream().flatMap(Mixin::outputTypesImpl);
+ }
+
+ @Override
+ public int outputSize() {
+ return numOutputs;
+ }
+
+ @Override
+ public Stream> paths() {
+ return prefixWithKeys(mixins);
+ }
+
+ @Override
+ public ValueProcessor processor(String context) {
+ return processor(context, false);
+ }
+
+ public ValueProcessor processor(String context, boolean isDiscriminated) {
+ final Map processors = new LinkedHashMap<>(mixins.size());
+ int ix = 0;
+ for (Entry> e : mixins.entrySet()) {
+ final ObjectField field = e.getKey();
+ final Mixin> opts = e.getValue();
+ final int numTypes = opts.outputSize();
+ final ValueProcessor fieldProcessor = opts.processor(context + "/" + field.name());
+ processors.put(field, fieldProcessor);
+ ix += numTypes;
+ }
+ if (ix != outputSize()) {
+ throw new IllegalStateException();
+ }
+ return processorImpl(processors, isDiscriminated);
+ }
+
+ @Override
+ RepeaterProcessor repeaterProcessor() {
+ final Map processors = new LinkedHashMap<>(mixins.size());
+ int ix = 0;
+ for (Entry> e : mixins.entrySet()) {
+ final ObjectField field = e.getKey();
+ final Mixin> opts = e.getValue();
+ final int numTypes = opts.outputSize();
+ final RepeaterProcessor fieldProcessor = opts.repeaterProcessor();
+ processors.put(field, fieldProcessor);
+ ix += numTypes;
+ }
+ if (ix != outputSize()) {
+ throw new IllegalStateException();
+ }
+ return new ObjectValueRepeaterProcessor(processors);
+ }
+
+ private boolean allCaseSensitive() {
+ return options.fields().stream().allMatch(ObjectField::caseSensitive);
+ }
+
+ ObjectValueFieldProcessor processorImpl(Map fields, boolean isDiscriminatedObject) {
+ return new ObjectValueFieldProcessor(fields, isDiscriminatedObject);
+ }
+
+ final class ObjectValueFieldProcessor extends ContextAwareDelegateBase implements ValueProcessor, FieldProcessor {
+ private final Map fields;
+ private final Map map;
+ private final boolean isDiscriminated;
+
+ ObjectValueFieldProcessor(Map fields, boolean isDiscriminated) {
+ super(fields.values());
+ this.fields = fields;
+ this.map = allCaseSensitive()
+ ? new HashMap<>()
+ : new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ this.isDiscriminated = isDiscriminated;
+ for (Entry e : fields.entrySet()) {
+ final ObjectField field = e.getKey();
+ map.put(field.name(), field);
+ for (String alias : field.aliases()) {
+ map.put(alias, field);
+ }
+ }
+ this.visited = new HashSet<>(fields.size());
+ }
+
+ private ObjectField lookupField(String fieldName) {
+ final ObjectField field = map.get(fieldName);
+ if (field == null) {
+ return null;
+ }
+ if (!field.caseSensitive()) {
+ return field;
+ }
+ // Need to handle the case where some fields are case-insensitive, but this one is _not_.
+ if (field.name().equals(fieldName) || field.aliases().contains(fieldName)) {
+ return field;
+ }
+ return null;
+ }
+
+ private ValueProcessor processor(ObjectField options) {
+ return Objects.requireNonNull(fields.get(options));
+ }
+
+ @Override
+ public void processCurrentValue(JsonParser parser) throws IOException {
+ // In the normal case, we expect to be at START_OBJECT or VALUE_NULL.
+ // In the discriminated case, we expect to already be inside an object (FIELD_NAME or END_OBJECT).
+ switch (parser.currentToken()) {
+ case START_OBJECT:
+ if (isDiscriminated) {
+ throw unexpectedToken(parser);
+ }
+ if (parser.nextToken() == JsonToken.END_OBJECT) {
+ processEmptyObject(parser);
+ return;
+ }
+ if (!parser.hasToken(JsonToken.FIELD_NAME)) {
+ throw new IllegalStateException();
+ }
+ processObjectFields(parser);
+ return;
+ case VALUE_NULL:
+ if (isDiscriminated) {
+ throw unexpectedToken(parser);
+ }
+ processNullObject(parser);
+ return;
+ case FIELD_NAME:
+ if (!isDiscriminated) {
+ throw unexpectedToken(parser);
+ }
+ processObjectFields(parser);
+ return;
+ case END_OBJECT:
+ if (!isDiscriminated) {
+ throw unexpectedToken(parser);
+ }
+ processEmptyObject(parser);
+ return;
+ default:
+ throw unexpectedToken(parser);
+ }
+ }
+
+ @Override
+ public void processMissing(JsonParser parser) throws IOException {
+ checkMissingAllowed(parser);
+ for (Entry entry : fields.entrySet()) {
+ processMissingField(entry.getKey(), entry.getValue(), parser);
+ }
+ }
+
+ private void processNullObject(JsonParser parser) throws IOException {
+ checkNullAllowed(parser);
+ for (Entry entry : fields.entrySet()) {
+ processField(entry.getKey(), entry.getValue(), parser);
+ }
+ }
+
+ private void processEmptyObject(JsonParser parser) throws IOException {
+ // This logic should be equivalent to processObjectFields, but where we know there are no fields
+ for (Entry entry : fields.entrySet()) {
+ processMissingField(entry.getKey(), entry.getValue(), parser);
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------------------
+
+ private final Set visited;
+
+ private void processObjectFields(JsonParser parser) throws IOException {
+ try {
+ FieldProcessor.processFields(parser, this);
+ if (visited.size() == fields.size()) {
+ // All fields visited, none missing
+ return;
+ }
+ for (Entry e : fields.entrySet()) {
+ if (!visited.contains(e.getKey())) {
+ processMissingField(e.getKey(), e.getValue(), parser);
+ }
+ }
+ } finally {
+ visited.clear();
+ }
+ }
+
+ @Override
+ public void process(String fieldName, JsonParser parser) throws IOException {
+ final ObjectField field = lookupField(fieldName);
+ if (field == null) {
+ if (!options.allowUnknownFields()) {
+ throw new ValueAwareException(String.format("Unknown field '%s' not allowed", fieldName),
+ parser.currentLocation(), options);
+ }
+ parser.skipChildren();
+ } else if (visited.add(field)) {
+ // First time seeing field
+ processField(field, processor(field), parser);
+ } else if (field.repeatedBehavior() == RepeatedBehavior.USE_FIRST) {
+ parser.skipChildren();
+ } else {
+ throw new ValueAwareException(String.format("Field '%s' has already been visited", fieldName),
+ parser.currentLocation(), options);
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------------------
+
+ private void processField(ObjectField field, ValueProcessor processor, JsonParser parser)
+ throws ValueAwareException {
+ try {
+ processor.processCurrentValue(parser);
+ } catch (IOException | RuntimeException e) {
+ throw new ValueAwareException(String.format("Unable to process field '%s'", field.name()),
+ parser.currentLocation(), e, options);
+ }
+ }
+
+ private void processMissingField(ObjectField field, ValueProcessor processor, JsonParser parser)
+ throws ValueAwareException {
+ try {
+ processor.processMissing(parser);
+ } catch (IOException | RuntimeException e) {
+ throw new ValueAwareException(String.format("Unable to process field '%s'", field.name()),
+ parser.currentLocation(), e, options);
+ }
+ }
+ }
+
+ final class ObjectValueRepeaterProcessor extends ContextAwareDelegateBase
+ implements RepeaterProcessor, Context, FieldProcessor {
+ private final Map fields;
+ private final Map contexts;
+ private final Map map;
+
+ public ObjectValueRepeaterProcessor(Map fields) {
+ super(fields.values());
+ this.fields = Objects.requireNonNull(fields);
+ contexts = new LinkedHashMap<>(fields.size());
+ for (Entry e : fields.entrySet()) {
+ contexts.put(e.getKey(), e.getValue().context());
+ }
+ this.map = allCaseSensitive()
+ ? new HashMap<>()
+ : new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (Entry e : fields.entrySet()) {
+ final ObjectField field = e.getKey();
+ map.put(field.name(), field);
+ for (String alias : field.aliases()) {
+ map.put(alias, field);
+ }
+ }
+ this.visited = new HashSet<>(fields.size());
+ }
+
+ private Collection