/*
* Copyright © 2015 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.data.schema.Schema;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Table;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
/**
* Decodes an object from a {@link Row} object fetched from a {@link Table}. Assumes that objects
* fetched are records. All fields are columns in the row, with simple types stored as their byte representation.
* Complex types (arrays, maps, records, enum) are not supported.
*
* @param <T> the type of object to read
*/
// suppress warnings that come from unboxing of objects that we validate are not null
@SuppressWarnings("ConstantConditions")
public class ReflectionRowReader<T> extends ReflectionReader<Row, T> {
private static final Schema NULL_SCHEMA = Schema.of(Schema.Type.NULL);
private List<String> fieldNames;
private int index;
public ReflectionRowReader(Schema schema, TypeToken<T> type) {
super(schema, type);
Preconditions.checkArgument(schema.getType() == Schema.Type.RECORD, "Target schema must be a record.");
for (Schema.Field field : schema.getFields()) {
Preconditions.checkArgument(
field.getSchema().isSimpleOrNullableSimple(),
"Target schema must only contain simple fields (boolean, int, long, float, double, bytes, string)");
}
}
@SuppressWarnings("unchecked")
public T read(Row row, Schema sourceSchema) throws IOException {
Preconditions.checkArgument(sourceSchema.getType() == Schema.Type.RECORD, "Source schema must be a record.");
initializeRead(sourceSchema);
try {
Object record = create(type);
for (Schema.Field sourceField : sourceSchema.getFields()) {
String sourceFieldName = sourceField.getName();
Schema.Field targetField = schema.getField(sourceFieldName);
if (targetField == null) {
advanceField();
continue;
}
FieldAccessor fieldAccessor = getFieldAccessor(type, sourceFieldName);
fieldAccessor.set(record, read(row, sourceField.getSchema(),
targetField.getSchema(), TypeToken.of(fieldAccessor.getType())));
}
return (T) record;
} catch (Exception e) {
throw propagate(e);
}
}
@Override
protected Object readNull(Row row) throws IOException {
advanceField();
return null;
}
@Override
protected boolean readBool(Row row) throws IOException {
String name = getCurrentField();
Boolean val = row.getBoolean(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected int readInt(Row row) throws IOException {
String name = getCurrentField();
Integer val = row.getInt(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected long readLong(Row row) throws IOException {
String name = getCurrentField();
Long val = row.getLong(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected float readFloat(Row row) throws IOException {
String name = getCurrentField();
Float val = row.getFloat(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected double readDouble(Row row) throws IOException {
String name = getCurrentField();
Double val = row.getDouble(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected String readString(Row row) throws IOException {
String name = getCurrentField();
String val = row.getString(name);
validateNotNull(val, name);
advanceField();
return val;
}
@Override
protected ByteBuffer readBytes(Row row) throws IOException {
String name = getCurrentField();
byte[] val = row.get(name);
validateNotNull(val, name);
advanceField();
return ByteBuffer.wrap(val);
}
@Override
protected Object readEnum(Row row, Schema sourceSchema, Schema targetSchema,
TypeToken<?> targetTypeToken) throws IOException {
throw new UnsupportedOperationException("Enums are not supported.");
}
@Override
protected Object readUnion(Row row, Schema sourceSchema, Schema targetSchema,
TypeToken<?> targetTypeToken) throws IOException {
// assumption is that unions are only possible if they represent a nullable.
if (!sourceSchema.isNullable()) {
throw new UnsupportedOperationException("Unions that do not represent nullables are not supported.");
}
String name = getCurrentField();
Schema sourceValueSchema = row.get(name) == null ? NULL_SCHEMA : sourceSchema.getNonNullable();
if (targetSchema.getType() == Schema.Type.UNION) {
for (Schema targetValueSchema : targetSchema.getUnionSchemas()) {
try {
return read(row, 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(row, sourceValueSchema, targetSchema, targetTypeToken);
}
}
@Override
protected Object readArray(Row row, Schema sourceSchema, Schema targetSchema,
TypeToken<?> targetTypeToken) throws IOException {
throw new UnsupportedOperationException("Arrays are not supported.");
}
@Override
protected Object readMap(Row row, Schema sourceSchema, Schema targetSchema,
TypeToken<?> targetTypeToken) throws IOException {
throw new UnsupportedOperationException("Maps are not supported.");
}
@Override
protected Object readRecord(Row row, Schema sourceSchema, Schema targetSchema,
TypeToken<?> targetTypeToken) throws IOException {
throw new UnsupportedOperationException("Records are not supported.");
}
protected String getCurrentField() {
return fieldNames.get(index);
}
protected void advanceField() {
index++;
}
// check that the value is not null and throw an exception if it is.
// Nullable types depend on this behavior to work correctly, as they cycle through
// possible types and catch IOExceptions if the type doesn't work out.
protected void validateNotNull(Object val, String column) throws IOException {
if (val == null) {
throw new IOException("No value for " + column + " exists.");
}
}
protected void initializeRead(Schema sourceSchema) {
List<Schema.Field> schemaFields = sourceSchema.getFields();
int numFields = schemaFields.size();
Preconditions.checkArgument(numFields > 0, "Record must contain at least one field.");
this.fieldNames = Lists.newArrayListWithCapacity(numFields);
for (Schema.Field schemaField : schemaFields) {
this.fieldNames.add(schemaField.getName());
}
this.index = 0;
}
}