/*
* Copyright © 2014 Cask Data, Inc.
*
* Licensed 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 co.cask.cdap.internal.io;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.common.io.Decoder;
import co.cask.cdap.common.lang.Instantiator;
import co.cask.cdap.common.lang.InstantiatorFactory;
import co.cask.cdap.common.stream.DefaultStreamEvent;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import com.google.common.reflect.TypeToken;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
/**
* Reflection based Datnum Reader.
*
* @param <T> type T reader
*/
public final class ReflectionDatumReader<T> implements DatumReader<T> {
private final Schema schema;
private final TypeToken<T> type;
private final Map<Class<?>, Instantiator<?>> creators;
private final InstantiatorFactory creatorFactory;
private final FieldAccessorFactory fieldAccessorFactory;
@SuppressWarnings("unchecked")
public ReflectionDatumReader(Schema schema, TypeToken<T> type) {
this.schema = schema;
this.type = type.getRawType().equals(StreamEvent.class)
? (TypeToken<T>) TypeToken.of(DefaultStreamEvent.class) : type;
this.creatorFactory = new InstantiatorFactory(true);
this.creators = Maps.newIdentityHashMap();
this.fieldAccessorFactory = new ReflectionFieldAccessorFactory();
}
@SuppressWarnings("unchecked")
@Override
public T read(Decoder decoder, Schema sourceSchema) throws IOException {
return (T) read(decoder, sourceSchema, schema, type);
}
private Object read(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
if (sourceSchema.getType() != Schema.Type.UNION && targetSchema.getType() == Schema.Type.UNION) {
// Try every target schemas
for (Schema schema : targetSchema.getUnionSchemas()) {
try {
return doRead(decoder, sourceSchema, schema, targetTypeToken);
} catch (IOException e) {
// Continue;
}
}
throw new IOException(String.format("No matching schema to resolve %s to %s", sourceSchema, targetSchema));
}
return doRead(decoder, sourceSchema, targetSchema, targetTypeToken);
}
private Object doRead(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
Schema.Type sourceType = sourceSchema.getType();
Schema.Type targetType = targetSchema.getType();
switch(sourceType) {
case NULL:
check(sourceType == targetType, "Fails to resolve %s to %s", sourceType, targetType);
return decoder.readNull();
case BYTES:
check(sourceType == targetType, "Fails to resolve %s to %s", sourceType, targetType);
return readBytes(decoder, targetTypeToken);
case ENUM:
String enumValue = sourceSchema.getEnumValue(decoder.readInt());
check(targetSchema.getEnumValues().contains(enumValue), "Enum value '%s' missing in target.", enumValue);
try {
return targetTypeToken.getRawType().getMethod("valueOf", String.class).invoke(null, enumValue);
} catch (Exception e) {
throw new IOException(e);
}
case ARRAY:
check(sourceType == targetType, "Fails to resolve %s to %s", sourceType, targetType);
return readArray(decoder, sourceSchema, targetSchema, targetTypeToken);
case MAP:
check(sourceType == targetType, "Fails to resolve %s to %s", sourceType, targetType);
return readMap(decoder, sourceSchema, targetSchema, targetTypeToken);
case RECORD:
check(sourceType == targetType, "Fails to resolve %s to %s", sourceType, targetType);
return readRecord(decoder, sourceSchema, targetSchema, targetTypeToken);
case UNION:
return readUnion(decoder, sourceSchema, targetSchema, targetTypeToken);
}
// For simple type other than NULL and BYTES
if (sourceType.isSimpleType()) {
return resolveType(decoder, sourceType, targetType, targetTypeToken);
}
throw new IOException(String.format("Fails to resolve %s to %s", sourceSchema, targetSchema));
}
private Object readBytes(Decoder decoder, TypeToken<?> targetTypeToken) throws IOException {
ByteBuffer buffer = decoder.readBytes();
if (targetTypeToken.getRawType().equals(byte[].class)) {
if (buffer.hasArray()) {
byte[] array = buffer.array();
if (buffer.remaining() == array.length) {
return array;
}
byte[] bytes = new byte[buffer.remaining()];
System.arraycopy(array, buffer.arrayOffset() + buffer.position(), bytes, 0, buffer.remaining());
return bytes;
} else {
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return bytes;
}
} else if (targetTypeToken.getRawType().equals(UUID.class) && buffer.remaining() == Longs.BYTES * 2) {
return new UUID(buffer.getLong(), buffer.getLong());
}
return buffer;
}
@SuppressWarnings("unchecked")
private Object readArray(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
TypeToken<?> componentType = null;
if (targetTypeToken.isArray()) {
componentType = targetTypeToken.getComponentType();
} else if (Collection.class.isAssignableFrom(targetTypeToken.getRawType())) {
Type type = targetTypeToken.getType();
check(type instanceof ParameterizedType, "Only parameterized type is supported for collection.");
componentType = TypeToken.of(((ParameterizedType) type).getActualTypeArguments()[0]);
}
check(componentType != null, "Only array or collection type is support for array value.");
int len = decoder.readInt();
Collection<Object> collection = (Collection<Object>) create(targetTypeToken);
while (len != 0) {
for (int i = 0; i < len; i++) {
collection.add(read(decoder, sourceSchema.getComponentSchema(),
targetSchema.getComponentSchema(), componentType)
);
}
len = decoder.readInt();
}
if (targetTypeToken.isArray()) {
Object array = Array.newInstance(targetTypeToken.getComponentType().getRawType(), collection.size());
int idx = 0;
for (Object obj : collection) {
Array.set(array, idx++, obj);
}
return array;
}
return collection;
}
@SuppressWarnings("unchecked")
private Map<Object, Object> readMap(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
check(Map.class.isAssignableFrom(targetTypeToken.getRawType()), "Only map type is supported for map data.");
Type type = targetTypeToken.getType();
Preconditions.checkArgument(type instanceof ParameterizedType, "Only parameterized map is supported.");
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
int len = decoder.readInt();
Map<Object, Object> map = (Map<Object, Object>) create(targetTypeToken);
while (len != 0) {
for (int i = 0; i < len; i++) {
Map.Entry<Schema, Schema> sourceEntry = sourceSchema.getMapSchema();
Map.Entry<Schema, Schema> targetEntry = targetSchema.getMapSchema();
map.put(read(decoder, sourceEntry.getKey(), targetEntry.getKey(), TypeToken.of(typeArgs[0])),
read(decoder, sourceEntry.getValue(), targetEntry.getValue(), TypeToken.of(typeArgs[1])));
}
len = decoder.readInt();
}
return map;
}
private Object readRecord(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
try {
Object record = create(targetTypeToken);
for (Schema.Field sourceField : sourceSchema.getFields()) {
Schema.Field targetField = targetSchema.getField(sourceField.getName());
if (targetField == null) {
skip(decoder, sourceField.getSchema());
continue;
}
FieldAccessor fieldAccessor = fieldAccessorFactory.getFieldAccessor(targetTypeToken, sourceField.getName());
fieldAccessor.set(record,
read(decoder, sourceField.getSchema(), targetField.getSchema(), fieldAccessor.getType()));
}
return record;
} catch (Exception e) {
throw propagate(e);
}
}
private Object readUnion(Decoder decoder, Schema sourceSchema,
Schema targetSchema, TypeToken<?> targetTypeToken) throws IOException {
int idx = decoder.readInt();
Schema sourceValueSchema = sourceSchema.getUnionSchemas().get(idx);
if (targetSchema.getType() == Schema.Type.UNION) {
try {
// A simple optimization to try resolve before resorting to linearly try the union schema.
Schema targetValueSchema = targetSchema.getUnionSchema(idx);
if (targetValueSchema != null && targetValueSchema.getType() == sourceValueSchema.getType()) {
return read(decoder, sourceValueSchema, targetValueSchema, targetTypeToken);
}
} catch (IOException e) {
// OK to ignore it, as we'll do union schema resolution
}
for (Schema targetValueSchema : targetSchema.getUnionSchemas()) {
try {
return read(decoder, sourceValueSchema, targetValueSchema, targetTypeToken);
} catch (IOException e) {
// It's ok to have exception here, as we'll keep trying until exhausted the target union.
}
}
throw new IOException(String.format("Fail to resolve %s to %s", sourceSchema, targetSchema));
} else {
return read(decoder, sourceValueSchema, targetSchema, targetTypeToken);
}
}
private void skip(Decoder decoder, Schema schema) throws IOException {
switch (schema.getType()) {
case NULL:
break;
case BOOLEAN:
decoder.readBool();
break;
case INT:
decoder.readInt();
break;
case LONG:
decoder.readLong();
break;
case FLOAT:
decoder.skipFloat();
break;
case DOUBLE:
decoder.skipDouble();
break;
case BYTES:
decoder.skipBytes();
break;
case STRING:
decoder.skipString();
break;
case ENUM:
decoder.readInt();
break;
case ARRAY:
skipArray(decoder, schema.getComponentSchema());
break;
case MAP:
skipMap(decoder, schema.getMapSchema());
break;
case RECORD:
skipRecord(decoder, schema);
break;
case UNION:
skip(decoder, schema.getUnionSchema(decoder.readInt()));
break;
}
}
private void skipArray(Decoder decoder, Schema componentSchema) throws IOException {
int len = decoder.readInt();
while (len != 0) {
skip(decoder, componentSchema);
len = decoder.readInt();
}
}
private void skipMap(Decoder decoder, Map.Entry<Schema, Schema> mapSchema) throws IOException {
int len = decoder.readInt();
while (len != 0) {
skip(decoder, mapSchema.getKey());
skip(decoder, mapSchema.getValue());
len = decoder.readInt();
}
}
private void skipRecord(Decoder decoder, Schema recordSchema) throws IOException {
for (Schema.Field field : recordSchema.getFields()) {
skip(decoder, field.getSchema());
}
}
private Object resolveType(Decoder decoder, Schema.Type sourceType,
Schema.Type targetType, TypeToken<?> targetTypeToken) throws IOException {
switch(sourceType) {
case BOOLEAN:
switch(targetType) {
case BOOLEAN:
return decoder.readBool();
case STRING:
return String.valueOf(decoder.readBool());
}
break;
case INT:
switch(targetType) {
case INT:
Class<?> targetClass = targetTypeToken.getRawType();
int value = decoder.readInt();
if (targetClass.equals(byte.class) || targetClass.equals(Byte.class)) {
return (byte) value;
}
if (targetClass.equals(char.class) || targetClass.equals(Character.class)) {
return (char) value;
}
if (targetClass.equals(short.class) || targetClass.equals(Short.class)) {
return (short) value;
}
return value;
case LONG:
return (long) decoder.readInt();
case FLOAT:
return (float) decoder.readInt();
case DOUBLE:
return (double) decoder.readInt();
case STRING:
return String.valueOf(decoder.readInt());
}
break;
case LONG:
switch(targetType) {
case LONG:
return decoder.readLong();
case FLOAT:
return (float) decoder.readLong();
case DOUBLE:
return (double) decoder.readLong();
case STRING:
return String.valueOf(decoder.readLong());
}
break;
case FLOAT:
switch(targetType) {
case FLOAT:
return decoder.readFloat();
case DOUBLE:
return (double) decoder.readFloat();
case STRING:
return String.valueOf(decoder.readFloat());
}
break;
case DOUBLE:
switch(targetType) {
case DOUBLE:
return decoder.readDouble();
case STRING:
return String.valueOf(decoder.readDouble());
}
break;
case STRING:
switch(targetType) {
case STRING:
String str = decoder.readString();
Class<?> targetClass = targetTypeToken.getRawType();
if (targetClass.equals(URI.class)) {
return URI.create(str);
} else if (targetClass.equals(URL.class)) {
return new URL(str);
}
return str;
}
break;
}
throw new IOException("Fail to resolve type " + sourceType + " to type " + targetType);
}
private void check(boolean condition, String message, Object... objs) throws IOException {
if (!condition) {
throw new IOException(String.format(message, objs));
}
}
private IOException propagate(Throwable t) throws IOException {
if (t instanceof IOException) {
throw (IOException) t;
}
throw new IOException(t);
}
private Object create(TypeToken<?> type) {
Class<?> rawType = type.getRawType();
Instantiator<?> creator = creators.get(rawType);
if (creator == null) {
creator = creatorFactory.get(type);
creators.put(rawType, creator);
}
return creator.create();
}
}