/*
* Copyright © 2014-2016 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.data.schema.UnsupportedTypeException;
import co.cask.cdap.internal.guava.reflect.TypeToken;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This class uses Java Reflection to inspect fields in any Java class to generate RECORD schema.
* <p/>
* <p>
* If the given type is a class, it will uses the class fields (includes all the fields in parent classes)
* to generate the schema. All fields, no matter what access it is would be included, except transient or
* synthetic one.
* </p>
* <p/>
* <p>
* If the given type is an interface, it will uses all getter methods (methods that prefix with "get" or "is",
* followed by a name with no arguments) to generate fields for the record schema.
* E.g. for the method {@code String getFirstName()}, a field name "firstName" of type String would be generated.
* </p>
*/
public final class ReflectionSchemaGenerator extends AbstractSchemaGenerator {
private final boolean isNullableByDefault;
public ReflectionSchemaGenerator(boolean isNullableByDefault) {
this.isNullableByDefault = isNullableByDefault;
}
public ReflectionSchemaGenerator() {
this(true);
}
@Override
protected Schema generateRecord(TypeToken<?> typeToken, Set<String> knowRecords, boolean acceptRecursion)
throws UnsupportedTypeException {
String recordName = typeToken.getRawType().getName();
Map<String, TypeToken<?>> recordFieldTypes =
typeToken.getRawType().isInterface() ?
collectByMethods(typeToken, new TreeMap<String, TypeToken<?>>()) :
collectByFields(typeToken, new TreeMap<String, TypeToken<?>>());
// Recursively generate field type schema.
List<Schema.Field> fields = new ArrayList<>();
for (Map.Entry<String, TypeToken<?>> fieldType : recordFieldTypes.entrySet()) {
Set<String> records = new HashSet<>(knowRecords);
records.add(recordName);
Schema fieldSchema = doGenerate(fieldType.getValue(), records, acceptRecursion);
if (!fieldType.getValue().getRawType().isPrimitive()) {
boolean isNotNull = typeToken.getRawType().isAnnotationPresent(Nonnull.class);
boolean isNull = typeToken.getRawType().isAnnotationPresent(Nullable.class);
// For non-primitive, allows "null" value
// i) if it is nullable by default and notnull annotation is not present
// ii) if it is not nullable by default and nullable annotation is present
if ((isNullableByDefault && !isNotNull) || (!isNullableByDefault && isNull)) {
fieldSchema = Schema.unionOf(fieldSchema, Schema.of(Schema.Type.NULL));
}
}
fields.add(Schema.Field.of(fieldType.getKey(), fieldSchema));
}
return Schema.recordOf(recordName, Collections.unmodifiableList(fields));
}
private Map<String, TypeToken<?>> collectByFields(TypeToken<?> typeToken, Map<String, TypeToken<?>> fieldTypes) {
// Collect the field types
for (TypeToken<?> classType : typeToken.getTypes().classes()) {
Class<?> rawType = classType.getRawType();
if (rawType.equals(Object.class)) {
// Ignore all object fields
continue;
}
for (Field field : rawType.getDeclaredFields()) {
int modifiers = field.getModifiers();
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers) || field.isSynthetic()) {
continue;
}
TypeToken<?> fieldType = classType.resolveType(field.getGenericType());
fieldTypes.put(field.getName(), fieldType);
}
}
return fieldTypes;
}
private Map<String, TypeToken<?>> collectByMethods(TypeToken<?> typeToken, Map<String, TypeToken<?>> fieldTypes) {
for (Method method : typeToken.getRawType().getMethods()) {
if (method.getDeclaringClass().equals(Object.class)) {
// Ignore all object methods
continue;
}
String methodName = method.getName();
if (!(methodName.startsWith("get") || methodName.startsWith("is"))
|| method.isSynthetic() || Modifier.isStatic(method.getModifiers())
|| method.getParameterTypes().length != 0) {
// Ignore not getter methods
continue;
}
String fieldName = methodName.startsWith("get") ?
methodName.substring("get".length()) : methodName.substring("is".length());
if (fieldName.isEmpty()) {
continue;
}
fieldName = String.format("%c%s", Character.toLowerCase(fieldName.charAt(0)), fieldName.substring(1));
if (fieldTypes.containsKey(fieldName)) {
continue;
}
TypeToken<?> fieldType = typeToken.resolveType(method.getGenericReturnType());
fieldTypes.put(fieldName, fieldType);
}
return fieldTypes;
}
}