package com.ctriposs.baiji.generic; import com.ctriposs.baiji.exception.BaijiRuntimeException; import com.ctriposs.baiji.exception.BaijiTypeException; import com.ctriposs.baiji.io.Encoder; import com.ctriposs.baiji.schema.*; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * A general purpose writer of data from Baiji streams. This writer analyzes the writer schema * when constructed so that writes can be more efficient. Once constructed, a writer can be reused or shared among threads * to avoid incurring more resolution costs. * * @param <T> */ public abstract class PreresolvingDatumWriter<T> implements DatumWriter<T> { private final Schema _schema; private final ItemWriter _writer; private final ArrayAccess _arrayAccess; private final MapAccess _mapAccess; private final Map<RecordSchema, ItemWriter> _recordWriters = new HashMap<RecordSchema, ItemWriter>(); protected PreresolvingDatumWriter(Schema schema, ArrayAccess arrayAccess, MapAccess mapAccess) { _schema = schema; _arrayAccess = arrayAccess; _mapAccess = mapAccess; _writer = resolveWriter(schema); } @Override public Schema getSchema() { return _schema; } @Override public void write(T datum, Encoder out) throws IOException { _writer.write(datum, out); } private ItemWriter resolveWriter(Schema schema) { switch (schema.getType()) { case NULL: return NullItemWriter.INSTANCE; case BOOLEAN: return BooleanItemWriter.INSTANCE; case INT: return IntItemWriter.INSTANCE; case LONG: return LongItemWriter.INSTANCE; case FLOAT: return FloatItemWriter.INSTANCE; case DOUBLE: return DoubleItemWriter.INSTANCE; case STRING: return StringItemWriter.INSTANCE; case BYTES: return BytesItemWriter.INSTANCE; case RECORD: return resolveRecord((RecordSchema) schema); case ENUM: return resolveEnum((EnumSchema) schema); case ARRAY: return resolveArray((ArraySchema) schema); case MAP: return resolveMap((MapSchema) schema); case UNION: return resolveUnion((UnionSchema) schema); default: return new ErrorItemWriter(schema); } } /** * Serialized a record using the given RecordSchema. It uses GetField method * to extract the field value from the given object. * * @param schema The RecordSchema to use for serialization * @return */ private ItemWriter resolveRecord(RecordSchema schema) { ItemWriter recordWriter = _recordWriters.get(schema); if (recordWriter != null) { return recordWriter; } RecordFieldWriter[] writeSteps = new RecordFieldWriter[schema.size()]; recordWriter = new RecordFieldsWriter(writeSteps); _recordWriters.put(schema, recordWriter); int index = 0; for (Field field : schema) { RecordFieldWriter record = new RecordFieldWriter(resolveWriter(field.getSchema()), field); writeSteps[index++] = record; } return recordWriter; } protected static class RecordFieldWriter { public ItemWriter _fieldWriter; public Field _field; public RecordFieldWriter(ItemWriter fieldWriter, Field field) { _fieldWriter = fieldWriter; _field = field; } } protected abstract void writeRecordFields(Object record, RecordFieldWriter[] writers, Encoder encoder) throws IOException; /** * Serializes an enumeration. * * @param es The EnumSchema for serialization * @return */ protected abstract ItemWriter resolveEnum(EnumSchema es); /** * Serialized an array. The default implementation calls EnsureArrayObject() to ascertain that the * given value is an array. It then calls GetArrayLength() and GetArrayElement() * to access the members of the array and then serialize them. * * @param schema The ArraySchema for serialization * @return */ protected ItemWriter resolveArray(ArraySchema schema) { ItemWriter itemWriter = resolveWriter(schema.getItemSchema()); return new ArrayItemWriter(itemWriter); } private ItemWriter resolveMap(MapSchema mapSchema) { ItemWriter itemWriter = resolveWriter(mapSchema.getValueSchema()); return new MapItemWriter(itemWriter); } private ItemWriter resolveUnion(UnionSchema unionSchema) { Schema[] branchSchemas = unionSchema.getSchemas().toArray(new Schema[0]); ItemWriter[] branchWriters = new ItemWriter[branchSchemas.length]; int branchIndex = 0; for (Schema branch : branchSchemas) { branchWriters[branchIndex++] = resolveWriter(branch); } return new UnionItemWriter(unionSchema, branchSchemas, branchWriters); } private class UnionItemWriter implements ItemWriter { private final UnionSchema _schema; private final Schema[] _branches; private final ItemWriter[] _branchWriters; public UnionItemWriter(UnionSchema schema, Schema[] branches, ItemWriter[] branchWriters) { _schema = schema; _branches = branches; _branchWriters = branchWriters; } @Override public void write(Object value, Encoder encoder) throws IOException { int index = resolveBranch(value); encoder.writeUnionIndex(index); _branchWriters[index].write(value, encoder); } /** * Finds the branch within the given UnionSchema that matches the given object. The default implementation * calls unionBranchMatches() method in the order of branches within the UnionSchema. If nothing matches, throws * an exception. * * @param obj The object that should be used in matching * @return */ protected int resolveBranch(Object obj) { for (int i = 0; i < _branches.length; i++) { if (unionBranchMatches(_branches[i], obj)) return i; } throw new BaijiRuntimeException("Cannot find a match for " + obj.getClass() + " in " + _schema); } } protected static BaijiRuntimeException typeMismatch(Object obj, String schemaType, String type) { return new BaijiRuntimeException(type + " required to write against " + schemaType + " schema but found " + (null == obj ? "null" : obj.getClass().toString())); } protected abstract boolean unionBranchMatches(Schema sc, Object obj); protected static interface ItemWriter { void write(Object value, Encoder encoder) throws IOException; } private static class NullItemWriter implements ItemWriter { private static final NullItemWriter INSTANCE = new NullItemWriter(); @Override public void write(Object value, Encoder encoder) { if (value != null) { throw typeMismatch(value, "null", "null"); } } } private static class BooleanItemWriter implements ItemWriter { private static final BooleanItemWriter INSTANCE = new BooleanItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof Boolean)) { throw typeMismatch(value, SchemaType.BOOLEAN.toString(), Boolean.class.toString()); } encoder.writeBoolean((Boolean) value); } } private static class IntItemWriter implements ItemWriter { private static final IntItemWriter INSTANCE = new IntItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof Integer)) { throw typeMismatch(value, SchemaType.INT.toString(), Integer.class.toString()); } encoder.writeInt((Integer) value); } } private static class LongItemWriter implements ItemWriter { private static final LongItemWriter INSTANCE = new LongItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof Long)) { throw typeMismatch(value, SchemaType.LONG.toString(), Long.class.toString()); } encoder.writeLong((Long) value); } } private static class FloatItemWriter implements ItemWriter { private static final FloatItemWriter INSTANCE = new FloatItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof Float)) { throw typeMismatch(value, SchemaType.FLOAT.toString(), Float.class.toString()); } encoder.writeFloat((Float) value); } } private static class DoubleItemWriter implements ItemWriter { private static final DoubleItemWriter INSTANCE = new DoubleItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof Double)) { throw typeMismatch(value, SchemaType.DOUBLE.toString(), Double.class.toString()); } encoder.writeDouble((Double) value); } } private static class StringItemWriter implements ItemWriter { private static final StringItemWriter INSTANCE = new StringItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof String)) { throw typeMismatch(value, SchemaType.STRING.toString(), String.class.toString()); } encoder.writeString((String) value); } } private static class BytesItemWriter implements ItemWriter { private static final BytesItemWriter INSTANCE = new BytesItemWriter(); @Override public void write(Object value, Encoder encoder) throws IOException { if (!(value instanceof byte[])) { throw typeMismatch(value, SchemaType.BYTES.toString(), byte[].class.toString()); } encoder.writeBytes((byte[]) value); } } private class RecordFieldsWriter implements ItemWriter { private final RecordFieldWriter[] _fieldWriters; public RecordFieldsWriter(RecordFieldWriter[] fieldWriters) { _fieldWriters = fieldWriters; } @Override public void write(Object value, Encoder encoder) throws IOException { writeRecordFields(value, _fieldWriters, encoder); } } private class ArrayItemWriter implements ItemWriter { private final ItemWriter _itemWriter; public ArrayItemWriter(ItemWriter itemWriter) { _itemWriter = itemWriter; } @Override public void write(Object value, Encoder encoder) throws IOException { _arrayAccess.ensureArrayObject(value); long l = _arrayAccess.getArrayLength(value); encoder.writeArrayStart(); encoder.setItemCount(l); _arrayAccess.writeArrayValues(value, _itemWriter, encoder); encoder.writeArrayEnd(); } } private class MapItemWriter implements ItemWriter { private final ItemWriter _valueWriter; public MapItemWriter(ItemWriter valueWriter) { _valueWriter = valueWriter; } @Override public void write(Object value, Encoder encoder) throws IOException { _mapAccess.ensureMapObject(value); encoder.writeMapStart(); encoder.setItemCount(_mapAccess.getMapSize(value)); _mapAccess.writeMapValues(value, _valueWriter, encoder); encoder.writeMapEnd(); } } private static class ErrorItemWriter implements ItemWriter { private final Schema _schema; public ErrorItemWriter(Schema schema) { _schema = schema; } @Override public void write(Object value, Encoder encoder) { throw new BaijiTypeException("Not a " + _schema + ": " + value); } } protected static interface EnumAccess { void writeEnum(Object value); } protected static interface ArrayAccess { /** * Checks if the given object is an array. If it is a valid array, this function returns normally. Otherwise, * it throws an exception. The default implementation checks if the value is an array. * * @param value */ void ensureArrayObject(Object value); /** * Returns the length of an array. The default implementation requires the object * to be an array of objects and returns its length. The default implementation * guarantees that ensureArrayObject() has been called on the value before this * function is called. * * @param value The object whose array length is required * @return The array length of the given object */ long getArrayLength(Object value); /** * Returns the element at the given index from the given array object. The default implementation * requires that the value is an object array and returns the element in that array. The defaul implementation * guarantees that ensureArrayObject() has been called on the value before this * function is called. * * @param array The array object * @param valueWriter * @param encoder * @throws IOException */ void writeArrayValues(Object array, ItemWriter valueWriter, Encoder encoder) throws IOException; } protected static interface MapAccess { /** * Checks if the given object is a map. If it is a valid map, this function returns normally. Otherwise, * it throws an exception. The default implementation checks if the value is an IDictionary{string, object}. * * @param value */ void ensureMapObject(Object value); /** * Returns the size of the map object. The default implementation guarantees that EnsureMapObject has been * successfully called with the given value. The default implementation requires the value * to be a {@link java.util.Map} and returns the number of elements in it. * * @param value The map object whose size is desired * @return The size of the given map object */ long getMapSize(Object value); /** * Returns the contents of the given map object. The default implementation guarantees that EnsureMapObject * has been called with the given value. The default implementation of this method requires that * the value is a {@link java.util.Map} and returns its contents. * * @param map The map object whose size is desired * @param valueWriter * @param encoder * @throws IOException */ void writeMapValues(Object map, ItemWriter valueWriter, Encoder encoder) throws IOException; } protected static class DefaultMapAccess implements MapAccess { public DefaultMapAccess() { } public void ensureMapObject(Object value) { if (!(value instanceof Map)) { throw typeMismatch(value, "map", "Map"); } } public long getMapSize(Object value) { return ((Map) value).size(); } public void writeMapValues(Object map, ItemWriter valueWriter, Encoder encoder) throws IOException { for (Map.Entry entry : ((Map<?, ?>) map).entrySet()) { encoder.startItem(); encoder.writeString(entry.getKey().toString()); valueWriter.write(entry.getValue(), encoder); } } } }