package org.bson.codecs.pojo;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.pojo.annotations.BsonCreator;
import org.bson.codecs.pojo.annotations.BsonDiscriminator;
import org.bson.codecs.pojo.annotations.BsonId;
import org.bson.codecs.pojo.annotations.BsonIgnore;
import org.bson.codecs.pojo.annotations.BsonProperty;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import static java.lang.String.format;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
import static org.bson.codecs.pojo.PojoBuilderHelper.createPropertyModelBuilder;
final class ConventionAnnotationImpl implements Convention {
@Override
public void apply(final ClassModelBuilder<?> classModelBuilder) {
for (final Annotation annotation : classModelBuilder.getAnnotations()) {
processClassAnnotation(classModelBuilder, annotation);
}
for (PropertyModelBuilder<?> propertyModelBuilder : classModelBuilder.getPropertyModelBuilders()) {
processPropertyAnnotations(classModelBuilder, propertyModelBuilder);
}
processCreatorAnnotation(classModelBuilder);
cleanPropertyBuilders(classModelBuilder);
}
private void processClassAnnotation(final ClassModelBuilder<?> classModelBuilder, final Annotation annotation) {
if (annotation instanceof BsonDiscriminator) {
BsonDiscriminator discriminator = (BsonDiscriminator) annotation;
String key = discriminator.key();
if (!key.equals("")) {
classModelBuilder.discriminatorKey(key);
}
String name = discriminator.value();
if (!name.equals("")) {
classModelBuilder.discriminator(name);
}
classModelBuilder.enableDiscriminator(true);
}
}
private void processPropertyAnnotations(final ClassModelBuilder<?> classModelBuilder,
final PropertyModelBuilder<?> propertyModelBuilder) {
for (Annotation annotation : propertyModelBuilder.getReadAnnotations()) {
if (annotation instanceof BsonProperty) {
BsonProperty bsonProperty = (BsonProperty) annotation;
if (!"".equals(bsonProperty.value())) {
propertyModelBuilder.readName(bsonProperty.value());
}
propertyModelBuilder.discriminatorEnabled(bsonProperty.useDiscriminator());
if (propertyModelBuilder.getName().equals(classModelBuilder.getIdPropertyName())) {
classModelBuilder.idPropertyName(null);
}
} else if (annotation instanceof BsonId) {
classModelBuilder.idPropertyName(propertyModelBuilder.getName());
} else if (annotation instanceof BsonIgnore) {
propertyModelBuilder.readName(null);
}
}
for (Annotation annotation : propertyModelBuilder.getWriteAnnotations()) {
if (annotation instanceof BsonProperty) {
BsonProperty bsonProperty = (BsonProperty) annotation;
if (!"".equals(bsonProperty.value())) {
propertyModelBuilder.writeName(bsonProperty.value());
}
} else if (annotation instanceof BsonIgnore) {
propertyModelBuilder.writeName(null);
}
}
}
@SuppressWarnings("unchecked")
private <T> void processCreatorAnnotation(final ClassModelBuilder<T> classModelBuilder) {
Class<T> clazz = classModelBuilder.getType();
CreatorExecutable<T> creatorExecutable = null;
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
if (isPublic(constructor.getModifiers()) && !constructor.isSynthetic()) {
for (Annotation annotation : constructor.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(BsonCreator.class)) {
if (creatorExecutable != null) {
throw new CodecConfigurationException("Found multiple constructors annotated with @BsonCreator");
}
creatorExecutable = new CreatorExecutable<T>(clazz, (Constructor<T>) constructor);
}
}
}
}
Class<?> bsonCreatorClass = clazz;
boolean foundStaticBsonCreatorMethod = false;
while (bsonCreatorClass != null && !foundStaticBsonCreatorMethod) {
for (Method method : bsonCreatorClass.getDeclaredMethods()) {
if (isStatic(method.getModifiers()) && !method.isSynthetic() && !method.isBridge()) {
for (Annotation annotation : method.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(BsonCreator.class)) {
if (creatorExecutable != null) {
throw new CodecConfigurationException("Found multiple constructors / methods annotated with @BsonCreator");
} else if (!bsonCreatorClass.isAssignableFrom(method.getReturnType())) {
throw new CodecConfigurationException(
format("Invalid method annotated with @BsonCreator. Returns '%s', expected %s",
method.getReturnType(), bsonCreatorClass));
}
creatorExecutable = new CreatorExecutable<T>(clazz, method);
foundStaticBsonCreatorMethod = true;
}
}
}
}
bsonCreatorClass = bsonCreatorClass.getSuperclass();
}
if (creatorExecutable != null) {
List<BsonProperty> properties = creatorExecutable.getProperties();
List<Class<?>> parameterTypes = creatorExecutable.getParameterTypes();
List<Type> parameterGenericTypes = creatorExecutable.getParameterGenericTypes();
if (properties.size() != parameterTypes.size()) {
throw creatorExecutable.getError(clazz, "All parameters in the @BsonCreator method / constructor must be annotated "
+ "with a @BsonProperty.");
}
for (int i = 0; i < properties.size(); i++) {
boolean isIdProperty = creatorExecutable.getIdPropertyIndex() != null && creatorExecutable.getIdPropertyIndex().equals(i);
Class<?> parameterType = parameterTypes.get(i);
Type genericType = parameterGenericTypes.get(i);
PropertyModelBuilder<?> propertyModelBuilder = null;
if (isIdProperty) {
propertyModelBuilder = classModelBuilder.getProperty(classModelBuilder.getIdPropertyName());
} else {
BsonProperty bsonProperty = properties.get(i);
for (PropertyModelBuilder<?> builder : classModelBuilder.getPropertyModelBuilders()) {
if (bsonProperty.value().equals(builder.getWriteName())) {
propertyModelBuilder = builder;
break;
} else if (bsonProperty.value().equals(builder.getReadName())) {
propertyModelBuilder = builder;
}
}
if (propertyModelBuilder == null) {
propertyModelBuilder = classModelBuilder.getProperty(bsonProperty.value());
}
if (propertyModelBuilder == null) {
propertyModelBuilder = addCreatorPropertyToClassModelBuilder(classModelBuilder, bsonProperty.value(),
parameterType);
} else {
if (!bsonProperty.value().equals(propertyModelBuilder.getName())) {
propertyModelBuilder.writeName(bsonProperty.value());
}
tryToExpandToGenericType(parameterType, propertyModelBuilder, genericType);
}
}
if (!propertyModelBuilder.getTypeData().isAssignableFrom(parameterType)) {
throw creatorExecutable.getError(clazz, format("Invalid Property type for '%s'. Expected %s, found %s.",
propertyModelBuilder.getWriteName(), propertyModelBuilder.getTypeData().getType(), parameterType));
}
}
classModelBuilder.instanceCreatorFactory(new InstanceCreatorFactoryImpl<T>(creatorExecutable));
}
}
@SuppressWarnings("unchecked")
private static <T> void tryToExpandToGenericType(final Class<?> parameterType, final PropertyModelBuilder<T> propertyModelBuilder,
final Type genericType) {
if (parameterType.isAssignableFrom(propertyModelBuilder.getTypeData().getType())) {
propertyModelBuilder.typeData(TypeData.newInstance(genericType, (Class<T>) parameterType));
}
}
private <T, S> PropertyModelBuilder<S> addCreatorPropertyToClassModelBuilder(final ClassModelBuilder<T> classModelBuilder,
final String name,
final Class<S> clazz) {
PropertyModelBuilder<S> propertyModelBuilder = createPropertyModelBuilder(new PropertyMetadata<S>(name,
classModelBuilder.getType().getSimpleName(), TypeData.builder(clazz).build())).readName(null).writeName(name);
classModelBuilder.addProperty(propertyModelBuilder);
return propertyModelBuilder;
}
private void cleanPropertyBuilders(final ClassModelBuilder<?> classModelBuilder) {
List<String> propertiesToRemove = new ArrayList<String>();
for (PropertyModelBuilder<?> propertyModelBuilder : classModelBuilder.getPropertyModelBuilders()) {
if (!propertyModelBuilder.isReadable() && !propertyModelBuilder.isWritable()) {
propertiesToRemove.add(propertyModelBuilder.getName());
}
}
for (String propertyName : propertiesToRemove) {
classModelBuilder.removeProperty(propertyName);
}
}
}