This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new c3cd3c70ad2 Fix TDigest accumulator serialization through generic
serdes for capacity-preserving state (#19017)
c3cd3c70ad2 is described below
commit c3cd3c70ad264665555b7204fc973e4adeb09c7d
Author: Xiang Fu <[email protected]>
AuthorDate: Wed Jul 22 04:29:25 2026 -0700
Fix TDigest accumulator serialization through generic serdes for
capacity-preserving state (#19017)
* Fix TDigest accumulator serialization through generic serdes for
capacity-preserving state
PercentileTDigestAccumulator now overrides byteSize()/asBytes() to emit its
mixed-capacity-safe serialize() bytes, so ObjectSerDeUtils.TDIGEST_SER_DE
and
CustomSerDeUtils.TDIGEST_SER_DE stay readable by plain
MergingDigest.fromBytes
readers (percentileRawTDigest final results previously emitted verbose bytes
with more centroids than a fresh reader allocates, failing with
ArrayIndexOutOfBoundsException). Also routes percentileSmartTDigest through
the
accumulator, completing the #18996 routing.
* Address review comments: null-safe merge, pending write-through in
asBytes, static test imports
---
.../PercentileSmartTDigestAggregationFunction.java | 34 +++++---
.../function/PercentileTDigestAccumulator.java | 45 +++++++++--
.../PercentileTDigestAggregationFunction.java | 9 +--
...ercentileRawTDigestAggregationFunctionTest.java | 90 ++++++++++++++++++++++
...centileSmartTDigestAggregationFunctionTest.java | 70 +++++++++++++++++
.../PercentileTDigestAggregationFunctionTest.java | 18 +++++
6 files changed, 241 insertions(+), 25 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunction.java
index 24ffd615ec5..539b3a7eea0 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunction.java
@@ -25,6 +25,7 @@ import it.unimi.dsi.fastutil.doubles.DoubleListIterator;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.common.CustomObject;
import org.apache.pinot.common.request.context.ExpressionContext;
@@ -184,7 +185,7 @@ public class PercentileSmartTDigestAggregationFunction
extends NullableSingleInp
}
private TDigest convertValueListToTDigest(DoubleArrayList valueList) {
- TDigest tDigest = TDigest.createMergingDigest(_compression);
+ TDigest tDigest = new PercentileTDigestAccumulator(_compression);
DoubleListIterator iterator = valueList.iterator();
while (iterator.hasNext()) {
tDigest.add(iterator.nextDouble());
@@ -265,12 +266,19 @@ public class PercentileSmartTDigestAggregationFunction
extends NullableSingleInp
}
@Override
- public Object merge(Object intermediateResult1, Object intermediateResult2) {
- if (intermediateResult1 instanceof TDigest) {
- return mergeIntoTDigest((TDigest) intermediateResult1,
intermediateResult2);
+ @Nullable
+ public Object merge(@Nullable Object intermediateResult1, @Nullable Object
intermediateResult2) {
+ if (intermediateResult1 == null) {
+ return intermediateResult2;
}
- if (intermediateResult2 instanceof TDigest) {
- return mergeIntoTDigest((TDigest) intermediateResult2,
intermediateResult1);
+ if (intermediateResult2 == null) {
+ return intermediateResult1;
+ }
+ if (intermediateResult1 instanceof PercentileTDigestAccumulator) {
+ return mergeIntoAccumulator((PercentileTDigestAccumulator)
intermediateResult1, intermediateResult2);
+ }
+ if (intermediateResult2 instanceof PercentileTDigestAccumulator) {
+ return mergeIntoAccumulator((PercentileTDigestAccumulator)
intermediateResult2, intermediateResult1);
}
DoubleArrayList valueList1 = (DoubleArrayList) intermediateResult1;
DoubleArrayList valueList2 = (DoubleArrayList) intermediateResult2;
@@ -278,17 +286,18 @@ public class PercentileSmartTDigestAggregationFunction
extends NullableSingleInp
return valueList1.size() > _threshold ?
convertValueListToTDigest(valueList1) : valueList1;
}
- private static TDigest mergeIntoTDigest(TDigest tDigest, Object
intermediateResult) {
+ private static PercentileTDigestAccumulator
mergeIntoAccumulator(PercentileTDigestAccumulator accumulator,
+ Object intermediateResult) {
if (intermediateResult instanceof TDigest) {
- tDigest.add((TDigest) intermediateResult);
+ accumulator.add((TDigest) intermediateResult);
} else {
DoubleArrayList valueList = (DoubleArrayList) intermediateResult;
DoubleListIterator iterator = valueList.iterator();
while (iterator.hasNext()) {
- tDigest.add(iterator.nextDouble());
+ accumulator.add(iterator.nextDouble());
}
}
- return tDigest;
+ return accumulator;
}
@Override
@@ -309,6 +318,11 @@ public class PercentileSmartTDigestAggregationFunction
extends NullableSingleInp
@Override
public Object deserializeIntermediateResult(CustomObject customObject) {
+ if (customObject.getType() ==
ObjectSerDeUtils.ObjectType.TDigest.getValue()) {
+ // Generic TDigest deserialization returns a plain MergingDigest. Keep
this function's TDigest intermediates as
+ // accumulators so subsequent merges retain the capacity-preserving
serialization path.
+ return
PercentileTDigestAccumulator.forSerializedTDigest(customObject.getBuffer());
+ }
return ObjectSerDeUtils.deserialize(customObject);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java
index 569cc269167..b0fdac2e49d 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAccumulator.java
@@ -37,8 +37,9 @@ import java.util.List;
/// its explicit capacity is required to decode an oversized compact digest.
///
/// The accumulator implements [TDigest] so process-local combine and
reduction can retain the primitive state.
-/// Quantile queries and CDFs read the primitive state directly. Centroid
iteration and serialization use the
-/// canonical library implementation returned from [#toTDigest()].
+/// Quantile queries and CDFs read the primitive state directly. Centroid
iteration uses the canonical library
+/// implementation returned from [#toTDigest()], while [#byteSize()] and
[#asBytes(ByteBuffer)] emit [#serialize()]
+/// bytes directly so generic `TDigest` serializers preserve
capacity-preserving state.
///
/// Serialized group state keeps its first digest pending, and allocates
primitive centroid buffers only when another
/// input must be merged. The raw-value buffer remains unallocated for
serialized state until a raw value is added.
@@ -476,9 +477,25 @@ final class PercentileTDigestAccumulator extends TDigest {
return _compression;
}
+ /// Returns the length of the [#serialize()] bytes rather than the
materialized [MergingDigest] byte size, so
+ /// generic `TDigest` serializers ([#byteSize()] followed by
[#asBytes(ByteBuffer)]) emit the capacity-preserving
+ /// encoding. Re-encoding through a materialized [MergingDigest] can produce
a verbose digest with more centroids
+ /// than a freshly allocated [MergingDigest] of the same compression can
hold, which readers reject with
+ /// [ArrayIndexOutOfBoundsException]. The length is computed without
materializing the bytes by mirroring the
+ /// [#serialize()] branches; the mutations here (flush, capacity
normalization) are idempotent, so the following
+ /// [#asBytes(ByteBuffer)] call writes exactly this many bytes.
@Override
public int byteSize() {
- return toTDigest().byteSize();
+ if (_pendingSerializedTDigest != null) {
+ return _pendingSerializedTDigest.length;
+ }
+ flush();
+ if (requiresCapacityPreservingEncoding()) {
+ checkCapacityPreservingCentroidCount();
+ return SMALL_HEADER_SIZE + SMALL_CENTROID_SIZE * _numCentroids;
+ }
+ normalizeCentroidCapacity();
+ return VERBOSE_HEADER_SIZE + VERBOSE_CENTROID_SIZE * _numCentroids;
}
@Override
@@ -486,9 +503,17 @@ final class PercentileTDigestAccumulator extends TDigest {
return toTDigest().smallByteSize();
}
+ /// Writes the [#serialize()] bytes; see [#byteSize()]. Bytes are always
written in big-endian order (the t-digest
+ /// wire order) regardless of the destination buffer's byte order, unlike
[MergingDigest#asBytes(ByteBuffer)].
@Override
public void asBytes(ByteBuffer buffer) {
- toTDigest().asBytes(buffer);
+ if (_pendingSerializedTDigest != null) {
+ // Same validation as serialize(), but write through without the
defensive clone.
+ getSerializedTotalWeight(_pendingSerializedTDigest);
+ buffer.put(_pendingSerializedTDigest);
+ return;
+ }
+ buffer.put(serialize());
}
@Override
@@ -610,10 +635,7 @@ final class PercentileTDigestAccumulator extends TDigest {
}
private byte[] toCapacityPreservingBytes() {
- if (_numCentroids > Short.MAX_VALUE) {
- throw new IllegalStateException("TDigest has too many centroids for
capacity-preserving encoding: "
- + _numCentroids);
- }
+ checkCapacityPreservingCentroidCount();
int mainCapacity = Math.min(Short.MAX_VALUE, Math.max(_numCentroids,
_serializedMainCapacity));
long defaultBufferCapacity =
Math.multiplyExact(DEFAULT_MERGE_BUFFER_MULTIPLIER, (long)
Math.ceil(_compression));
int bufferCapacity = Math.toIntExact(Math.min(Short.MAX_VALUE,
@@ -633,6 +655,13 @@ final class PercentileTDigestAccumulator extends TDigest {
return buffer.array();
}
+ private void checkCapacityPreservingCentroidCount() {
+ if (_numCentroids > Short.MAX_VALUE) {
+ throw new IllegalStateException("TDigest has too many centroids for
capacity-preserving encoding: "
+ + _numCentroids);
+ }
+ }
+
private void flush() {
if (_numRawValues == 0) {
return;
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java
index 8fd195c6c89..eacf5d91f5a 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunction.java
@@ -300,13 +300,8 @@ public class PercentileTDigestAggregationFunction extends
NullableSingleInputAgg
@Override
public SerializedIntermediateResult serializeIntermediateResult(TDigest
tDigest) {
- byte[] bytes;
- if (tDigest instanceof PercentileTDigestAccumulator) {
- bytes = ((PercentileTDigestAccumulator) tDigest).serialize();
- } else {
- bytes = ObjectSerDeUtils.TDIGEST_SER_DE.serialize(tDigest);
- }
- return new
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
bytes);
+ return new
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
+ ObjectSerDeUtils.TDIGEST_SER_DE.serialize(tDigest));
}
@Override
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileRawTDigestAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileRawTDigestAggregationFunctionTest.java
new file mode 100644
index 00000000000..911374148b5
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileRawTDigestAggregationFunctionTest.java
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import java.nio.ByteBuffer;
+import org.apache.pinot.common.CustomObject;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.core.common.ObjectSerDeUtils;
+import org.apache.pinot.spi.utils.BytesUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// Tests that the final result of `percentileRawTDigest` stays readable by a
plain t-digest
+/// [MergingDigest#fromBytes] reader when the intermediate
[PercentileTDigestAccumulator] holds
+/// capacity-preserving state (a digest with more centroids than a freshly
allocated
+/// [MergingDigest] of the same compression can hold).
+public class PercentileRawTDigestAggregationFunctionTest {
+ private static final ExpressionContext EXPRESSION =
ExpressionContext.forIdentifier("col");
+
+ @Test
+ public void testFinalResultReadableByPlainReaderForCapacityPreservingState()
{
+ int numCentroids = 51;
+ double compression = 20.0;
+ byte[] small = createSmallUnitCentroidDigest(numCentroids, compression,
60, 100);
+
+ // Sanity: the input itself is readable by a plain t-digest reader.
+ TDigest direct = MergingDigest.fromBytes(ByteBuffer.wrap(small));
+ assertEquals(direct.size(), numCentroids);
+
+ PercentileRawTDigestAggregationFunction function =
+ new PercentileRawTDigestAggregationFunction(EXPRESSION, 50.0, (int)
compression, false);
+ TDigest intermediateResult = function.deserializeIntermediateResult(
+ new CustomObject(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
ByteBuffer.wrap(small)));
+ assertTrue(intermediateResult instanceof PercentileTDigestAccumulator);
+
+ byte[] serialized =
BytesUtils.toBytes(function.extractFinalResult(intermediateResult).toString());
+
+ // Client side: a plain t-digest reader must be able to read the emitted
bytes without losing
+ // state. Before the fix this threw ArrayIndexOutOfBoundsException because
the final result was
+ // re-encoded as a verbose digest with more centroids than the reader
allocates.
+ TDigest roundTripped =
MergingDigest.fromBytes(ByteBuffer.wrap(serialized));
+ assertEquals(roundTripped.size(), numCentroids);
+ assertEquals(roundTripped.quantile(0.0), 0.0);
+ assertEquals(roundTripped.quantile(0.5), (numCentroids - 1.0) / 2.0, 1.0);
+ assertEquals(roundTripped.quantile(1.0), numCentroids - 1.0);
+ }
+
+ /// SMALL-encoded digest (encoding 2) with explicit main/buffer capacities,
unit-weight centroids
+ /// at means 0..numCentroids-1. This is the layout t-digest's `asSmallBytes`
produces and
+ /// `MergingDigest.fromBytes` accepts regardless of the default capacity for
the compression.
+ /// Package-private: shared with
[PercentileSmartTDigestAggregationFunctionTest].
+ static byte[] createSmallUnitCentroidDigest(int numCentroids, double
compression, int mainCapacity,
+ int bufferCapacity) {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 2 * Double.BYTES +
Float.BYTES + 3 * Short.BYTES
+ + 2 * Float.BYTES * numCentroids);
+ buffer.putInt(2);
+ buffer.putDouble(0.0);
+ buffer.putDouble(numCentroids - 1.0);
+ buffer.putFloat((float) compression);
+ buffer.putShort((short) mainCapacity);
+ buffer.putShort((short) bufferCapacity);
+ buffer.putShort((short) numCentroids);
+ for (int i = 0; i < numCentroids; i++) {
+ buffer.putFloat(1.0f);
+ buffer.putFloat(i);
+ }
+ return buffer.array();
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunctionTest.java
index 68a180ea886..4cb07afd996 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunctionTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileSmartTDigestAggregationFunctionTest.java
@@ -18,8 +18,78 @@
*/
package org.apache.pinot.core.query.aggregation.function;
+import com.tdunning.math.stats.MergingDigest;
+import com.tdunning.math.stats.TDigest;
+import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.pinot.common.CustomObject;
+import org.apache.pinot.common.request.Literal;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.core.common.ObjectSerDeUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
public class PercentileSmartTDigestAggregationFunctionTest {
+ private static final ExpressionContext EXPRESSION =
ExpressionContext.forIdentifier("col");
+
+ /// Exercises the complete capacity-preserving intermediate lifecycle:
pass-through serde, materialization by
+ /// merging another digest, and raw-list merging in both argument orders.
+ @Test
+ public void testCapacityPreservingIntermediateLifecycle() {
+ int numCentroids = 51;
+ byte[] small =
PercentileRawTDigestAggregationFunctionTest.createSmallUnitCentroidDigest(numCentroids,
20.0, 60,
+ 100);
+ PercentileSmartTDigestAggregationFunction function = createFunction();
+
+ Object intermediateResult = function.deserializeIntermediateResult(
+ new CustomObject(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
ByteBuffer.wrap(small)));
+ assertTrue(intermediateResult instanceof PercentileTDigestAccumulator);
+
+ AggregationFunction.SerializedIntermediateResult passThrough =
+ function.serializeIntermediateResult(intermediateResult);
+ assertEquals(passThrough.getType(),
ObjectSerDeUtils.ObjectType.TDigest.getValue());
+ // Before the fix this was a verbose encoding with 51 centroids, which a
plain
+ // MergingDigest.fromBytes reader rejects with
ArrayIndexOutOfBoundsException.
+
assertEquals(MergingDigest.fromBytes(ByteBuffer.wrap(passThrough.getBytes())).size(),
numCentroids);
+
+ byte[] empty =
ObjectSerDeUtils.TDIGEST_SER_DE.serialize(TDigest.createMergingDigest(20.0));
+ Object merged = function.merge(
+ intermediateResult,
+ function.deserializeIntermediateResult(
+ new CustomObject(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
ByteBuffer.wrap(empty))));
+ byte[] serialized =
function.serializeIntermediateResult(merged).getBytes();
+ assertEquals(ByteBuffer.wrap(serialized).getInt(), 2, "Expected small
(capacity-preserving) encoding");
+ assertEquals(((TDigest) merged).byteSize(), serialized.length);
+ TDigest roundTripped =
MergingDigest.fromBytes(ByteBuffer.wrap(serialized));
+ assertEquals(roundTripped.size(), numCentroids);
+ assertEquals(roundTripped.quantile(0.5), (numCentroids - 1.0) / 2.0, 1.0);
+
+ DoubleArrayList values = new DoubleArrayList(new double[]{0.0, 25.0,
50.0});
+ assertEquals(((TDigest) function.merge(merged, values)).size(),
numCentroids + 3L);
+ Object reverseOrder = function.merge(new DoubleArrayList(values),
function.deserializeIntermediateResult(
+ new CustomObject(ObjectSerDeUtils.ObjectType.TDigest.getValue(),
ByteBuffer.wrap(small))));
+ assertTrue(reverseOrder instanceof PercentileTDigestAccumulator);
+ assertEquals(((TDigest) reverseOrder).size(), numCentroids + 3L);
+ }
+
+ @Test
+ public void testMergeWithNullIntermediateResult() {
+ PercentileSmartTDigestAggregationFunction function = createFunction();
+ DoubleArrayList valueList = new DoubleArrayList(new double[]{1.0, 2.0});
+ assertEquals(function.merge(null, valueList), valueList);
+ assertEquals(function.merge(valueList, null), valueList);
+ assertEquals(function.merge(null, null), null);
+ }
+
+ private static PercentileSmartTDigestAggregationFunction createFunction() {
+ return new PercentileSmartTDigestAggregationFunction(
+ List.of(EXPRESSION,
ExpressionContext.forLiteral(Literal.doubleValue(50.0)),
+
ExpressionContext.forLiteral(Literal.stringValue("THRESHOLD=1;COMPRESSION=20"))),
false);
+ }
public static class WithHighThreshold extends
AbstractPercentileAggregationFunctionTest {
@Override
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java
index d94ec545cf8..75aa822abf3 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/PercentileTDigestAggregationFunctionTest.java
@@ -457,6 +457,24 @@ public class PercentileTDigestAggregationFunctionTest {
}
}
+ @Test
+ public void testCapacityPreservingByteSizeRejectsUnencodableCentroidCount()
+ throws ReflectiveOperationException {
+ PercentileTDigestAccumulator accumulator =
PercentileTDigestAccumulator.forReduction(20.0);
+ int unencodableCentroidCount = Short.MAX_VALUE + 1;
+ Field numCentroidsField =
PercentileTDigestAccumulator.class.getDeclaredField("_numCentroids");
+ numCentroidsField.setAccessible(true);
+ numCentroidsField.setInt(accumulator, unencodableCentroidCount);
+ Field serializedMainCapacityField =
+
PercentileTDigestAccumulator.class.getDeclaredField("_serializedMainCapacity");
+ serializedMainCapacityField.setAccessible(true);
+ serializedMainCapacityField.setInt(accumulator, unencodableCentroidCount);
+
+ IllegalStateException exception =
Assert.expectThrows(IllegalStateException.class, accumulator::byteSize);
+ Assert.assertEquals(exception.getMessage(),
+ "TDigest has too many centroids for capacity-preserving encoding: " +
unencodableCentroidCount);
+ }
+
@Test
public void
testSerializedGroupByMVSharedInputUsesCentroidCountForInitialCapacity()
throws ReflectiveOperationException {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]