package core.framework.impl.mongo;
import core.framework.api.mongo.Collection;
import core.framework.api.mongo.Id;
import core.framework.api.mongo.MongoEnumValue;
import core.framework.api.util.Exceptions;
import core.framework.api.util.Maps;
import core.framework.api.util.Sets;
import core.framework.impl.reflect.Fields;
import core.framework.impl.validate.type.DataTypeValidator;
import core.framework.impl.validate.type.TypeVisitor;
import org.bson.types.ObjectId;
import javax.xml.bind.annotation.XmlEnumValue;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Set;
/**
* @author neo
*/
public final class MongoClassValidator implements TypeVisitor {
private final DataTypeValidator validator;
private final Map<String, Set<String>> fields = Maps.newHashMap();
private boolean validateView;
private Field id;
public MongoClassValidator(Class<?> entityClass) {
validator = new DataTypeValidator(entityClass);
validator.allowedValueClass = this::allowedValueClass;
validator.allowChild = true;
validator.visitor = this;
}
public void validateEntityClass() {
validator.validate();
if (id == null) {
throw Exceptions.error("mongo entity class must have @Id field, class={}", validator.type.getTypeName());
}
}
public void validateViewClass() {
validateView = true;
validator.validate();
}
private boolean allowedValueClass(Class<?> valueClass) {
return String.class.equals(valueClass)
|| ObjectId.class.equals(valueClass)
|| Integer.class.equals(valueClass)
|| Boolean.class.equals(valueClass)
|| Long.class.equals(valueClass)
|| Double.class.equals(valueClass)
|| LocalDateTime.class.equals(valueClass)
|| ZonedDateTime.class.equals(valueClass)
|| valueClass.isEnum();
}
@Override
public void visitClass(Class<?> objectClass, String path) {
if (!validateView && path == null && !objectClass.isAnnotationPresent(Collection.class))
throw Exceptions.error("mongo entity class must have @Collection, class={}", objectClass.getCanonicalName());
}
@Override
public void visitField(Field field, String parentPath) {
if (field.isAnnotationPresent(Id.class)) {
validateId(field, parentPath == null);
} else {
core.framework.api.mongo.Field mongoField = field.getDeclaredAnnotation(core.framework.api.mongo.Field.class);
if (mongoField == null)
throw Exceptions.error("mongo entity field must have @Field, field={}", Fields.path(field));
String mongoFieldName = mongoField.name();
Set<String> fields = this.fields.computeIfAbsent(parentPath, key -> Sets.newHashSet());
if (fields.contains(mongoFieldName)) {
throw Exceptions.error("found duplicate field, field={}, mongoField={}", Fields.path(field), mongoFieldName);
}
fields.add(mongoFieldName);
Class<?> fieldClass = field.getType();
if (fieldClass.isEnum()) {
@SuppressWarnings("unchecked")
Class<? extends Enum<?>> enumClass = (Class<? extends Enum<?>>) fieldClass;
validateEnumClass(enumClass, field);
}
}
}
private <T extends Enum<?>> void validateEnumClass(Class<T> enumClass, Field field) {
T[] constants = enumClass.getEnumConstants();
Set<String> enumValues = Sets.newHashSet();
for (T constant : constants) {
try {
Field enumField = enumClass.getDeclaredField(constant.name());
MongoEnumValue enumValue = enumField.getDeclaredAnnotation(MongoEnumValue.class);
if (enumValue == null) {
throw Exceptions.error("mongo enum must have @MongoEnumValue, field={}, enum={}", Fields.path(field), Fields.path(enumField));
}
boolean added = enumValues.add(enumValue.value());
if (!added) {
throw Exceptions.error("mongo enum value must be unique, enum={}, value={}", enumClass.getCanonicalName() + "." + constant, enumValue.value());
}
if (enumField.isAnnotationPresent(XmlEnumValue.class)) {
throw Exceptions.error("mongo enum must not have jaxb annotation, please separate view and entity, field={}, enum={}", Fields.path(field), Fields.path(enumField));
}
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
}
private void validateId(Field field, boolean topLevel) {
if (topLevel) {
if (id != null)
throw Exceptions.error("mongo entity class must have only one @Id field, previous={}, current={}", Fields.path(id), Fields.path(field));
Class<?> fieldClass = field.getType();
if (!ObjectId.class.equals(fieldClass) && !String.class.equals(fieldClass)) {
throw Exceptions.error("@Id field must be either ObjectId or String, field={}, class={}", Fields.path(field), fieldClass.getCanonicalName());
}
id = field;
} else {
throw Exceptions.error("mongo nested entity class must not have @Id field, field={}", field);
}
}
}