package core.framework.impl.mongo;
import core.framework.api.mongo.Id;
import core.framework.api.util.Lists;
import core.framework.api.util.Sets;
import core.framework.api.util.Strings;
import core.framework.impl.code.CodeBuilder;
import core.framework.impl.code.DynamicInstanceBuilder;
import core.framework.impl.reflect.Classes;
import core.framework.impl.reflect.GenericTypes;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author neo
*/
final class EntityDecoderBuilder<T> {
final Map<String, String> methods = new LinkedHashMap<>();
final List<String> fields = Lists.newArrayList();
private final Class<T> entityClass;
private final Set<Class<? extends Enum<?>>> enumClasses = Sets.newHashSet();
private final String helper = EntityCodecHelper.class.getCanonicalName();
EntityDecoderBuilder(Class<T> entityClass) {
this.entityClass = entityClass;
}
public EntityDecoder<T> build() {
DynamicInstanceBuilder<EntityDecoder<T>> builder = new DynamicInstanceBuilder<>(EntityDecoder.class, EntityDecoder.class.getCanonicalName() + "$" + entityClass.getSimpleName());
fields.add("private final " + Logger.class.getCanonicalName() + " logger = " + LoggerFactory.class.getCanonicalName() + ".getLogger(" + EntityDecoder.class.getCanonicalName() + ".class);\n");
buildMethods();
fields.forEach(builder::addField);
methods.values().forEach(builder::addMethod);
return builder.build();
}
private void buildMethods() {
String methodName = decodeEntityMethod(entityClass);
CodeBuilder builder = new CodeBuilder()
.append("public Object decode(org.bson.BsonReader reader) {\n")
.indent(1).append("return {}(reader, \"\");\n", methodName)
.append("}");
methods.put("decode", builder.build());
}
private String decodeEntityMethod(Class<?> entityClass) {
String entityClassName = entityClass.getCanonicalName();
String methodName = "decode_" + entityClassName.replaceAll("\\.", "_");
if (methods.containsKey(methodName)) return methodName;
CodeBuilder builder = new CodeBuilder().append("public {} {}(org.bson.BsonReader reader, String parentField) {\n", entityClassName, methodName);
builder.indent(1).append("org.bson.BsonType currentType = reader.getCurrentBsonType();\n");
builder.indent(1).append("if (currentType != null && currentType == org.bson.BsonType.NULL) {\n");
builder.indent(2).append("reader.readNull();\n");
builder.indent(2).append("return null;\n");
builder.indent(1).append("}\n");
builder.indent(1).append("if (currentType != null && currentType != org.bson.BsonType.DOCUMENT) {\n");
builder.indent(2).append("logger.warn(\"unexpected field type, field={}, type={}\", parentField, currentType);\n");
builder.indent(2).append("reader.skipValue();\n");
builder.indent(2).append("return null;\n");
builder.indent(1).append("}\n");
builder.indent(1).append("{} entity = new {}();\n", entityClassName, entityClassName);
builder.indent(1).append("reader.readStartDocument();\n")
.indent(1).append("while (reader.readBsonType() != org.bson.BsonType.END_OF_DOCUMENT) {\n")
.indent(2).append("String fieldName = reader.readName();\n")
.indent(2).append("String fieldPath = parentField + \".\" + fieldName;\n");
for (Field field : Classes.instanceFields(entityClass)) {
decodeEntityField(builder, field);
}
builder.indent(2).append("logger.warn(\"undefined field, field={}, type={}\", fieldPath, reader.getCurrentBsonType());\n");
builder.indent(2).append("reader.skipValue();\n");
builder.indent(1).append("}\n");
builder.indent(1).append("reader.readEndDocument();\n");
builder.indent(1).append("return entity;\n");
builder.append("}\n");
methods.put(methodName, builder.build());
return methodName;
}
private void decodeEntityField(CodeBuilder builder, Field field) {
String fieldVariable = "entity." + field.getName();
Class<?> fieldClass = field.getType();
Type fieldType = field.getGenericType();
String mongoFieldName;
if (field.isAnnotationPresent(Id.class)) mongoFieldName = "_id";
else mongoFieldName = field.getDeclaredAnnotation(core.framework.api.mongo.Field.class).name();
builder.indent(2).append("if (\"{}\".equals(fieldName)) {\n", mongoFieldName);
if (Integer.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readInteger(reader, fieldPath);\n", fieldVariable, helper);
} else if (String.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readString(reader, fieldPath);\n", fieldVariable, helper);
} else if (Long.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readLong(reader, fieldPath);\n", fieldVariable, helper);
} else if (LocalDateTime.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readLocalDateTime(reader, fieldPath);\n", fieldVariable, helper);
} else if (ZonedDateTime.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readZonedDateTime(reader, fieldPath);\n", fieldVariable, helper);
} else if (fieldClass.isEnum()) {
String enumCodecVariable = registerEnumCodec(fieldClass);
builder.indent(3).append("{} = ({}) {}.decode(reader, null);\n", fieldVariable, fieldClass.getCanonicalName(), enumCodecVariable);
} else if (Double.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readDouble(reader, fieldPath);\n", fieldVariable, helper);
} else if (ObjectId.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readObjectId(reader, fieldPath);\n", fieldVariable, helper);
} else if (Boolean.class.equals(fieldClass)) {
builder.indent(3).append("{} = {}.readBoolean(reader, fieldPath);\n", fieldVariable, helper);
} else if (GenericTypes.isGenericList(fieldType)) {
String method = decodeListMethod(GenericTypes.listValueClass(fieldType));
builder.indent(3).append("{} = {}(reader, fieldPath);\n", fieldVariable, method);
} else if (GenericTypes.isGenericStringMap(fieldType)) {
String method = decodeMapMethod(GenericTypes.mapValueClass(fieldType));
builder.indent(3).append("{} = {}(reader, fieldPath);\n", fieldVariable, method);
} else {
String method = decodeEntityMethod(fieldClass);
builder.indent(3).append("{} = {}(reader, fieldPath);\n", fieldVariable, method);
}
builder.indent(3).append("continue;\n");
builder.indent(2).append("}\n");
}
private String decodeMapMethod(Class<?> valueClass) {
String valueClassName = valueClass.getCanonicalName();
String methodName = ("decode_" + Map.class.getCanonicalName() + "_" + valueClassName).replaceAll("\\.", "_");
if (methods.containsKey(methodName)) return methodName;
CodeBuilder builder = new CodeBuilder();
builder.append("private java.util.Map {}(org.bson.BsonReader reader, String parentField) {\n", methodName);
builder.indent(1).append("org.bson.BsonType currentType = reader.getCurrentBsonType();\n");
builder.indent(1).append("if (currentType == org.bson.BsonType.NULL) { reader.readNull(); return null; }\n");
builder.indent(1).append("if (currentType != org.bson.BsonType.DOCUMENT) {\n");
builder.indent(2).append("logger.warn(\"unexpected field type, field={}, type={}\", parentField, currentType);\n");
builder.indent(2).append("reader.skipValue();\n");
builder.indent(2).append("return null;\n");
builder.indent(1).append("}\n");
builder.indent(1).append("java.util.Map map = new java.util.LinkedHashMap();\n");
builder.indent(1).append("reader.readStartDocument();\n");
builder.indent(1).append("while (reader.readBsonType() != org.bson.BsonType.END_OF_DOCUMENT) {\n");
builder.indent(2).append("String fieldName = reader.readName();\n");
builder.indent(2).append("String fieldPath = parentField + \".\" + fieldName;\n");
if (Integer.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readInteger(reader, fieldPath));\n", helper);
} else if (String.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readString(reader, fieldPath));\n", helper);
} else if (Long.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readLong(reader, fieldPath));\n", helper);
} else if (LocalDateTime.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readLocalDateTime(reader, fieldPath));\n", helper);
} else if (ZonedDateTime.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readZonedDateTime(reader, fieldPath));\n", helper);
} else if (valueClass.isEnum()) {
String enumCodecVariable = registerEnumCodec(valueClass);
builder.indent(2).append("map.put(fieldName, {}.read(reader, fieldPath));\n", enumCodecVariable);
} else if (Double.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readDouble(reader, fieldPath));\n", helper);
} else if (ObjectId.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readObjectId(reader, fieldPath));\n", helper);
} else if (Boolean.class.equals(valueClass)) {
builder.indent(2).append("map.put(fieldName, {}.readBoolean(reader, fieldPath));\n", helper);
} else {
String method = decodeEntityMethod(valueClass);
builder.indent(2).append("map.put(fieldName, {}(reader, fieldPath));\n", method);
}
builder.indent(1).append("}\n");
builder.indent(1).append("reader.readEndDocument();\n");
builder.indent(1).append("return map;\n");
builder.append("}\n");
methods.put(methodName, builder.build());
return methodName;
}
private String decodeListMethod(Class<?> valueClass) {
String valueClassName = valueClass.getCanonicalName();
String methodName = ("decode_" + List.class.getCanonicalName() + "_" + valueClassName).replaceAll("\\.", "_");
if (methods.containsKey(methodName)) return methodName;
CodeBuilder builder = new CodeBuilder();
builder.append("private java.util.List {}(org.bson.BsonReader reader, String fieldPath) {\n", methodName);
builder.indent(1).append("org.bson.BsonType currentType = reader.getCurrentBsonType();\n");
builder.indent(1).append("if (currentType == org.bson.BsonType.NULL) {\n");
builder.indent(2).append("reader.readNull();\n");
builder.indent(2).append("return null;\n");
builder.indent(1).append("}\n");
builder.indent(1).append("if (currentType != org.bson.BsonType.ARRAY) {\n");
builder.indent(2).append("logger.warn(\"unexpected field type, field={}, type={}\", fieldPath, currentType);\n");
builder.indent(2).append("reader.skipValue();\n");
builder.indent(2).append("return null;\n");
builder.indent(1).append("}\n");
builder.indent(1).append("java.util.List list = new java.util.ArrayList();\n");
builder.indent(1).append("reader.readStartArray();\n");
builder.indent(1).append("while (reader.readBsonType() != org.bson.BsonType.END_OF_DOCUMENT) {\n");
if (Integer.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readInteger(reader, fieldPath));\n", helper);
} else if (String.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readString(reader, fieldPath));\n", helper);
} else if (Long.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readLong(reader, fieldPath));\n", helper);
} else if (LocalDateTime.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readLocalDateTime(reader, fieldPath));\n", helper);
} else if (ZonedDateTime.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readZonedDateTime(reader, fieldPath));\n", helper);
} else if (valueClass.isEnum()) {
String enumCodecVariable = registerEnumCodec(valueClass);
builder.indent(2).append("list.add({}.read(reader, fieldPath));\n", enumCodecVariable, valueClassName);
} else if (Double.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readDouble(reader, fieldPath));\n", helper);
} else if (ObjectId.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readObjectId(reader, fieldPath));\n", helper);
} else if (Boolean.class.equals(valueClass)) {
builder.indent(2).append("list.add({}.readBoolean(reader, fieldPath));\n", helper);
} else {
String method = decodeEntityMethod(valueClass);
builder.indent(2).append("list.add({}(reader, fieldPath));\n", method);
}
builder.indent(1).append("}\n");
builder.indent(1).append("reader.readEndArray();\n");
builder.indent(1).append("return list;\n");
builder.append("}\n");
methods.put(methodName, builder.build());
return methodName;
}
private String registerEnumCodec(Class<?> fieldClass) {
@SuppressWarnings("unchecked")
boolean added = enumClasses.add((Class<? extends Enum<?>>) fieldClass);
String fieldVariable = fieldClass.getCanonicalName().replaceAll("\\.", "_") + "Codec";
if (added) {
String field = Strings.format("private final {} {} = new {}({}.class);\n",
EnumCodec.class.getCanonicalName(),
fieldVariable,
EnumCodec.class.getCanonicalName(),
fieldClass.getCanonicalName());
fields.add(field);
}
return fieldVariable;
}
}