/** * 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.avro.io; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Random; import org.apache.avro.Schema; import org.apache.avro.util.Utf8; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @RunWith(Parameterized.class) public class TestValidatingIO { enum Encoding { BINARY, BLOCKING_BINARY, JSON, } private static final Logger LOG = LoggerFactory.getLogger(TestValidatingIO.class); private Encoding eEnc; private int iSkipL; private String sJsSch; private String sCl; public TestValidatingIO (Encoding enc, int skip, String js, String cls) { this.eEnc = enc; this.iSkipL = skip; this.sJsSch = js; this.sCl = cls; } private static final int COUNT = 1; @Test public void testMain() throws IOException { for (int i = 0; i < COUNT; i++) { testOnce(new Schema.Parser().parse(sJsSch), sCl, iSkipL, eEnc); } } private void testOnce(Schema schema, String calls, int skipLevel, Encoding encoding) throws IOException { Object[] values = randomValues(calls); print(eEnc, iSkipL, schema, schema, values, values); byte[] bytes = make(schema, calls, values, encoding); check(schema, bytes, calls, values, skipLevel, encoding); } public static byte[] make(Schema sc, String calls, Object[] values, Encoding encoding) throws IOException { EncoderFactory factory = EncoderFactory.get(); ByteArrayOutputStream ba = new ByteArrayOutputStream(); Encoder bvo = null; switch (encoding) { case BINARY: bvo = factory.binaryEncoder(ba, null); break; case BLOCKING_BINARY: bvo = factory.blockingBinaryEncoder(ba, null); break; case JSON: bvo = factory.jsonEncoder(sc, ba); break; } Encoder vo = factory.validatingEncoder(sc, bvo); generate(vo, calls, values); vo.flush(); return ba.toByteArray(); } public static class InputScanner { private final char[] chars; private int cpos = 0; public InputScanner(char[] chars) { this.chars = chars; } public boolean next() { if (cpos < chars.length) { cpos++; } return cpos != chars.length; } public char cur() { return chars[cpos]; } public boolean isDone() { return cpos == chars.length; } } public static void generate(Encoder vw, String calls, Object[] values) throws IOException { InputScanner cs = new InputScanner(calls.toCharArray()); int p = 0; while (! cs.isDone()) { char c = cs.cur(); cs.next(); switch (c) { case 'N': vw.writeNull(); break; case 'B': boolean b = (Boolean) values[p++]; vw.writeBoolean(b); break; case 'I': int ii = (Integer) values[p++]; vw.writeInt(ii); break; case 'L': long l = (Long) values[p++]; vw.writeLong(l); break; case 'F': float f = (Float) values[p++]; vw.writeFloat(f); break; case 'D': double d = (Double) values[p++]; vw.writeDouble(d); break; case 'S': { extractInt(cs); String s = (String) values[p++]; vw.writeString(new Utf8(s)); break; } case 'K': { extractInt(cs); String s = (String) values[p++]; vw.writeString(s); break; } case 'b': { extractInt(cs); byte[] bb = (byte[]) values[p++]; vw.writeBytes(bb); break; } case 'f': { extractInt(cs); byte[] bb = (byte[]) values[p++]; vw.writeFixed(bb); break; } case 'e': { int e = extractInt(cs); vw.writeEnum(e); break; } case '[': vw.writeArrayStart(); break; case ']': vw.writeArrayEnd(); break; case '{': vw.writeMapStart(); break; case '}': vw.writeMapEnd(); break; case 'c': vw.setItemCount(extractInt(cs)); break; case 's': vw.startItem(); break; case 'U': { vw.writeIndex(extractInt(cs)); break; } default: fail(); break; } } } public static Object[] randomValues(String calls) { Random r = new Random(0L); InputScanner cs = new InputScanner(calls.toCharArray()); List<Object> result = new ArrayList<Object>(); while (! cs.isDone()) { char c = cs.cur(); cs.next(); switch (c) { case 'N': break; case 'B': result.add(r.nextBoolean()); break; case 'I': result.add(r.nextInt()); break; case 'L': result.add(new Long(r.nextInt())); break; case 'F': result.add(new Float(r.nextInt())); break; case 'D': result.add(new Double(r.nextInt())); break; case 'S': case 'K': result.add(nextString(r, extractInt(cs))); break; case 'b': case 'f': result.add(nextBytes(r, extractInt(cs))); break; case 'e': case 'c': case 'U': extractInt(cs); case '[': case ']': case '{': case '}': case 's': break; default: fail(); break; } } return result.toArray(); } private static int extractInt(InputScanner sc) { int r = 0; while (! sc.isDone()) { if (Character.isDigit(sc.cur())) { r = r * 10 + sc.cur() - '0'; sc.next(); } else { break; } } return r; } private static byte[] nextBytes(Random r, int length) { byte[] bb = new byte[length]; r.nextBytes(bb); return bb; } private static String nextString(Random r, int length) { char[] cc = new char[length]; for (int i = 0; i < length; i++) { cc[i] = (char) ('A' + r.nextInt(26)); } return new String(cc); } private static void check(Schema sc, byte[] bytes, String calls, Object[] values, final int skipLevel, Encoding encoding) throws IOException { // dump(bytes); // System.out.println(new String(bytes, "UTF-8")); Decoder bvi = null; switch (encoding) { case BINARY: case BLOCKING_BINARY: bvi = DecoderFactory.get().binaryDecoder(bytes, null); break; case JSON: InputStream in = new ByteArrayInputStream(bytes); bvi = new JsonDecoder(sc, in); } Decoder vi = new ValidatingDecoder(sc, bvi); check(vi, calls, values, skipLevel); } public static void check(Decoder vi, String calls, Object[] values, final int skipLevel) throws IOException { InputScanner cs = new InputScanner(calls.toCharArray()); int p = 0; int level = 0; long[] counts = new long[100]; boolean[] isArray = new boolean[100]; boolean[] isEmpty = new boolean[100]; while (! cs.isDone()) { final char c = cs.cur(); cs.next(); switch (c) { case 'N': vi.readNull(); break; case 'B': assertEquals(values[p++], vi.readBoolean()); break; case 'I': assertEquals(values[p++], vi.readInt()); break; case 'L': assertEquals(values[p++], vi.readLong()); break; case 'F': if (!(values[p] instanceof Float)) fail(); float f = (Float) values[p++]; assertEquals(f, vi.readFloat(), Math.abs(f / 1000)); break; case 'D': if (!(values[p] instanceof Double)) fail(); double d = (Double) values[p++]; assertEquals(d, vi.readDouble(), Math.abs(d / 1000)); break; case 'S': extractInt(cs); if (level == skipLevel) { vi.skipString(); p++; } else { String s = (String) values[p++]; assertEquals(new Utf8(s), vi.readString(null)); } break; case 'K': extractInt(cs); if (level == skipLevel) { vi.skipString(); p++; } else { String s = (String) values[p++]; assertEquals(new Utf8(s), vi.readString(null)); } break; case 'b': extractInt(cs); if (level == skipLevel) { vi.skipBytes(); p++; } else { byte[] bb = (byte[]) values[p++]; ByteBuffer bb2 = vi.readBytes(null); byte[] actBytes = new byte[bb2.remaining()]; System.arraycopy(bb2.array(), bb2.position(), actBytes, 0, bb2.remaining()); assertArrayEquals(bb, actBytes); } break; case 'f': { int len = extractInt(cs); if (level == skipLevel) { vi.skipFixed(len); p++; } else { byte[] bb = (byte[]) values[p++]; byte[] actBytes = new byte[len]; vi.readFixed(actBytes); assertArrayEquals(bb, actBytes); } } break; case 'e': { int e = extractInt(cs); if (level == skipLevel) { vi.readEnum(); } else { assertEquals(e, vi.readEnum()); } } break; case '[': if (level == skipLevel) { p += skip(cs, vi, true); break; } else { level++; counts[level] = vi.readArrayStart(); isArray[level] = true; isEmpty[level] = counts[level] == 0; continue; } case '{': if (level == skipLevel) { p += skip(cs, vi, false); break; } else { level++; counts[level] = vi.readMapStart(); isArray[level] = false; isEmpty[level] = counts[level] == 0; continue; } case ']': assertEquals(0, counts[level]); if (! isEmpty[level]) { assertEquals(0, vi.arrayNext()); } level--; break; case '}': assertEquals(0, counts[level]); if (! isEmpty[level]) { assertEquals(0, vi.mapNext()); } level--; break; case 's': if (counts[level] == 0) { if (isArray[level]) { counts[level] = vi.arrayNext(); } else { counts[level] = vi.mapNext(); } } counts[level]--; continue; case 'c': extractInt(cs); continue; case 'U': { int idx = extractInt(cs); assertEquals(idx, vi.readIndex()); continue; } case 'R': ((ResolvingDecoder) vi).readFieldOrder(); continue; default: fail(); } } assertEquals(values.length, p); } private static int skip(InputScanner cs, Decoder vi, boolean isArray) throws IOException { final char end = isArray ? ']' : '}'; if (isArray) { assertEquals(0, vi.skipArray()); } else if (end == '}'){ assertEquals(0, vi.skipMap()); } int level = 0; int p = 0; while (! cs.isDone()) { char c = cs.cur(); cs.next(); switch (c) { case '[': case '{': ++level; break; case ']': case '}': if (c == end && level == 0) { return p; } level--; break; case 'B': case 'I': case 'L': case 'F': case 'D': case 'S': case 'K': case 'b': case 'f': case 'e': p++; break; } } throw new RuntimeException("Don't know how to skip"); } @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(convertTo2dArray(encodings, skipLevels, testSchemas())); } private static Object[][] encodings = new Object[][] { { Encoding.BINARY }, { Encoding.BLOCKING_BINARY }, { Encoding.JSON } }; private static Object[][] skipLevels = new Object[][] { { -1 }, { 0 }, { 1 }, { 2 }, }; public static Object[][] convertTo2dArray(final Object[][]... values) { ArrayList<Object[]> ret = new ArrayList<Object[]>(); Iterator<Object[]> iter = cartesian(values); while (iter.hasNext()) { Object[] objects = iter.next(); ret.add(objects); } Object[][] retArrays = new Object[ret.size()][]; for (int i = 0; i < ret.size(); i++) { retArrays[i] = ret.get(i); } return retArrays; } /** * Returns the Cartesian product of input sequences. */ public static Iterator<Object[]> cartesian(final Object[][]... values) { return new Iterator<Object[]>() { private int[] pos = new int[values.length]; @Override public boolean hasNext() { return pos[0] < values[0].length; } @Override public Object[] next() { Object[][] v = new Object[values.length][]; for (int i = 0; i < v.length; i++) { v[i] = values[i][pos[i]]; } for (int i = v.length - 1; i >= 0; i--) { if (++pos[i] == values[i].length) { if (i != 0) { pos[i] = 0; } } else { break; } } return concat(v); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /** * Concatenates the input sequences in order and forms a longer sequence. */ public static Object[] concat(Object[]... oo) { int l = 0; for (Object[] o : oo) { l += o.length; } Object[] result = new Object[l]; l = 0; for (Object[] o : oo) { System.arraycopy(o, 0, result, l, o.length); l += o.length; } return result; } /** * Pastes incoming tables to form a wider table. All incoming tables * should be of same height. */ static Object[][] paste(Object[][]... in) { Object[][] result = new Object[in[0].length][]; Object[][] cc = new Object[in.length][]; for (int i = 0; i < result.length; i++) { for (int j = 0; j < cc.length; j++) { cc[j] = in[j][i]; } result[i] = concat(cc); } return result; } public static Object[][] testSchemas() { /** * The first argument is a schema. * The second one is a sequence of (single character) mnemonics: * N null * B boolean * I int * L long * F float * D double * K followed by integer - key-name (and its length) in a map * S followed by integer - string and its length * b followed by integer - bytes and length * f followed by integer - fixed and length * c Number of items to follow in an array/map. * U followed by integer - Union and its branch * e followed by integer - Enum and its value * [ Start array * ] End array * { Start map * } End map * s start item */ return new Object[][] { { "\"null\"", "N" }, { "\"boolean\"", "B" }, { "\"int\"", "I" }, { "\"long\"", "L" }, { "\"float\"", "F" }, { "\"double\"", "D" }, { "\"string\"", "S0" }, { "\"string\"", "S10" }, { "\"bytes\"", "b0" }, { "\"bytes\"", "b10" }, { "{\"type\":\"fixed\", \"name\":\"fi\", \"size\": 1}", "f1" }, { "{\"type\":\"fixed\", \"name\":\"fi\", \"size\": 10}", "f10" }, { "{\"type\":\"enum\", \"name\":\"en\", \"symbols\":[\"v1\", \"v2\"]}", "e1" }, { "{\"type\":\"array\", \"items\": \"boolean\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"int\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"long\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"float\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"double\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"string\"}", "[]", }, { "{\"type\":\"array\", \"items\": \"bytes\"}", "[]", }, { "{\"type\":\"array\", \"items\":{\"type\":\"fixed\", " + "\"name\":\"fi\", \"size\": 10}}", "[]" }, { "{\"type\":\"array\", \"items\": \"boolean\"}", "[c1sB]" }, { "{\"type\":\"array\", \"items\": \"int\"}", "[c1sI]" }, { "{\"type\":\"array\", \"items\": \"long\"}", "[c1sL]" }, { "{\"type\":\"array\", \"items\": \"float\"}", "[c1sF]" }, { "{\"type\":\"array\", \"items\": \"double\"}", "[c1sD]" }, { "{\"type\":\"array\", \"items\": \"string\"}", "[c1sS10]" }, { "{\"type\":\"array\", \"items\": \"bytes\"}", "[c1sb10]" }, { "{\"type\":\"array\", \"items\": \"int\"}", "[c1sIc1sI]" }, { "{\"type\":\"array\", \"items\": \"int\"}", "[c2sIsI]" }, { "{\"type\":\"array\", \"items\":{\"type\":\"fixed\", " + "\"name\":\"fi\", \"size\": 10}}", "[c2sf10sf10]" }, { "{\"type\":\"map\", \"values\": \"boolean\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"int\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"long\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"float\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"double\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"string\"}", "{}" }, { "{\"type\":\"map\", \"values\": \"bytes\"}", "{}" }, { "{\"type\":\"map\", \"values\": " + "{\"type\":\"array\", \"items\":\"int\"}}", "{}" }, { "{\"type\":\"map\", \"values\": \"boolean\"}", "{c1sK5B}" }, { "{\"type\":\"map\", \"values\": \"int\"}", "{c1sK5I}" }, { "{\"type\":\"map\", \"values\": \"long\"}", "{c1sK5L}" }, { "{\"type\":\"map\", \"values\": \"float\"}", "{c1sK5F}" }, { "{\"type\":\"map\", \"values\": \"double\"}", "{c1sK5D}" }, { "{\"type\":\"map\", \"values\": \"string\"}", "{c1sK5S10}" }, { "{\"type\":\"map\", \"values\": \"bytes\"}", "{c1sK5b10}" }, { "{\"type\":\"map\", \"values\": " + "{\"type\":\"array\", \"items\":\"int\"}}", "{c1sK5[c3sIsIsI]}" }, { "{\"type\":\"map\", \"values\": \"boolean\"}", "{c1sK5Bc2sK5BsK5B}" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"boolean\"}]}", "B" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"int\"}]}", "I" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"long\"}]}", "L" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"float\"}]}", "F" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"double\"}]}", "D" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"string\"}]}", "S10" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f\", \"type\":\"bytes\"}]}", "b10" }, // multi-field records { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"int\"}," + "{\"name\":\"f2\", \"type\":\"double\"}," + "{\"name\":\"f3\", \"type\":\"string\"}]}", "IDS10" }, { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f0\", \"type\":\"null\"}," + "{\"name\":\"f1\", \"type\":\"boolean\"}," + "{\"name\":\"f2\", \"type\":\"int\"}," + "{\"name\":\"f3\", \"type\":\"long\"}," + "{\"name\":\"f4\", \"type\":\"float\"}," + "{\"name\":\"f5\", \"type\":\"double\"}," + "{\"name\":\"f6\", \"type\":\"string\"}," + "{\"name\":\"f7\", \"type\":\"bytes\"}]}", "NBILFDS10b25" }, // record of records { "{\"type\":\"record\",\"name\":\"outer\",\"fields\":[" + "{\"name\":\"f1\", \"type\":{\"type\":\"record\", " + "\"name\":\"inner\", \"fields\":[" + "{\"name\":\"g1\", \"type\":\"int\"}, {\"name\":\"g2\", " + "\"type\":\"double\"}]}}," + "{\"name\":\"f2\", \"type\":\"string\"}," + "{\"name\":\"f3\", \"type\":\"inner\"}]}", "IDS10ID" }, // record with array { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", " + "\"type\":{\"type\":\"array\", \"items\":\"int\"}}]}", "L[c1sI]" }, // record with map { "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", " + "\"type\":{\"type\":\"map\", \"values\":\"int\"}}]}", "L{c1sK5I}" }, // array of records { "{\"type\":\"array\", \"items\":" + "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", \"type\":\"null\"}]}}", "[c2sLNsLN]" }, { "{\"type\":\"array\", \"items\":" + "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", " + "\"type\":{\"type\":\"array\", \"items\":\"int\"}}]}}", "[c2sL[c1sI]sL[c2sIsI]]" }, { "{\"type\":\"array\", \"items\":" + "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", " + "\"type\":{\"type\":\"map\", \"values\":\"int\"}}]}}", "[c2sL{c1sK5I}sL{c2sK5IsK5I}]" }, { "{\"type\":\"array\", \"items\":" + "{\"type\":\"record\",\"name\":\"r\",\"fields\":[" + "{\"name\":\"f1\", \"type\":\"long\"}," + "{\"name\":\"f2\", " + "\"type\":[\"null\", \"int\"]}]}}", "[c2sLU0NsLU1I]" }, { "[\"boolean\"]", "U0B" }, { "[\"int\"]", "U0I" }, { "[\"long\"]", "U0L" }, { "[\"float\"]", "U0F" }, { "[\"double\"]", "U0D" }, { "[\"string\"]", "U0S10" }, { "[\"bytes\"]", "U0b10" }, { "[\"null\", \"int\"]", "U0N" }, { "[\"boolean\", \"int\"]", "U0B" }, { "[\"boolean\", \"int\"]", "U1I" }, { "[\"boolean\", {\"type\":\"array\", \"items\":\"int\"} ]", "U0B" }, { "[\"boolean\", {\"type\":\"array\", \"items\":\"int\"} ]", "U1[c1sI]" }, // Recursion { "{\"type\": \"record\", \"name\": \"Node\", \"fields\": [" + "{\"name\":\"label\", \"type\":\"string\"}," + "{\"name\":\"children\", \"type\":" + "{\"type\": \"array\", \"items\": \"Node\" }}]}", "S10[c1sS10[]]" }, { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": [" + "{\"name\":\"value\", \"type\":[\"null\", \"string\"," + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": [" + "{\"name\":\"car\", \"type\":\"Lisp\"}," + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}", "U0N"}, { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": [" + "{\"name\":\"value\", \"type\":[\"null\", \"string\"," + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": [" + "{\"name\":\"car\", \"type\":\"Lisp\"}," + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}", "U1S10"}, { "{\"type\": \"record\", \"name\": \"Lisp\", \"fields\": [" + "{\"name\":\"value\", \"type\":[\"null\", \"string\"," + "{\"type\": \"record\", \"name\": \"Cons\", \"fields\": [" + "{\"name\":\"car\", \"type\":\"Lisp\"}," + "{\"name\":\"cdr\", \"type\":\"Lisp\"}]}]}]}", "U2U1S10U0N"}, // Deep recursion { "{\"type\": \"record\", \"name\": \"Node\", \"fields\": [" + "{\"name\":\"children\", \"type\":" + "{\"type\": \"array\", \"items\": \"Node\" }}]}", "[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[c1s[]]]]]]]]]]]]" }, }; } static void dump(byte[] bb) { int col = 0; for (byte b : bb) { if (col % 16 == 0) { System.out.println(); } col++; System.out.print(Integer.toHexString(b & 0xff) + " "); } System.out.println(); } static void print(Encoding encoding, int skipLevel, Schema writerSchema, Schema readerSchema, Object[] writtenValues, Object[] expectedValues) { LOG.debug("{} Skip Level {}", encoding, skipLevel); printSchemaAndValues("Writer", writerSchema, writtenValues); printSchemaAndValues("Reader", readerSchema, expectedValues); } private static void printSchemaAndValues(String schemaType, Schema schema, Object[] values) { LOG.debug("{} Schema {}", schemaType, schema); for (Object value : values) { LOG.debug("{} -> {}", value, value.getClass().getSimpleName()); } } }