All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mercateo.common.rest.schemagen.generator.ObjectContext Maven / Gradle / Ivy

There is a newer version: 0.20.0
Show newest version
package com.mercateo.common.rest.schemagen.generator;

import static java.util.Objects.requireNonNull;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import javax.validation.Constraint;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.ws.rs.PathParam;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.ImmutableList;
import com.googlecode.gentyref.GenericTypeReflector;
import com.mercateo.common.rest.schemagen.IgnoreInRestSchema;
import com.mercateo.common.rest.schemagen.PropertySubType;
import com.mercateo.common.rest.schemagen.PropertySubTypeMapper;
import com.mercateo.common.rest.schemagen.PropertyType;
import com.mercateo.common.rest.schemagen.PropertyTypeMapper;
import com.mercateo.common.rest.schemagen.SchemaPropertyContext;
import com.mercateo.common.rest.schemagen.SizeConstraints;
import com.mercateo.common.rest.schemagen.ValueConstraints;
import com.mercateo.common.rest.schemagen.generictype.GenericClass;
import com.mercateo.common.rest.schemagen.generictype.GenericType;
import com.mercateo.common.rest.schemagen.plugin.IndividualSchemaGenerator;
import com.mercateo.common.rest.schemagen.plugin.PropertySchema;
import com.mercateo.common.rest.schemagen.types.ObjectWithSchema;

public class ObjectContext {

    private static final Set> IGNORE_ANNOTATIONS = new HashSet<>(Arrays
            .asList(JsonIgnore.class, IgnoreInRestSchema.class));

    private final GenericType type;

    private final PropertyType propertyType;

    private final T defaultValue;

    private final List allowedValues;

    private final boolean required;

    private final SizeConstraints sizeConstraints;

    private final ValueConstraints valueConstraints;

    private final Class schemaGenerator;

    private T currentValue;
    private PropertySubType propertySubType;

    ObjectContext(GenericType type, T defaultValue, List allowedValues, boolean required,
                  SizeConstraints sizeConstraints,
                  ValueConstraints valueConstraints, Class schemaGenerator, T currentValue) {
        this.type = requireNonNull(type);
        this.currentValue = currentValue;
        this.propertyType = PropertyTypeMapper.of(type);
        this.propertySubType = PropertySubTypeMapper.of(type, this.propertyType);
        this.defaultValue = defaultValue;
        this.allowedValues = allowedValues;
        this.required = required;
        this.sizeConstraints = sizeConstraints;
        this.valueConstraints = valueConstraints;
        this.schemaGenerator = schemaGenerator;
    }

    public static  Builder buildFor(Type type, Class clazz) {
        return new Builder<>(GenericType.of(type, clazz));
    }

    public static  Builder buildFor(Class clazz) {
        return new Builder<>(new GenericClass<>(clazz));
    }

    public static  Builder buildFor(GenericType type) {
        return new Builder<>(type);
    }

    private static  ObjectContext buildObjectContextForSuper(
            GenericType superType, List allowedValues, T defaultValue) {
        List newAllowedValues = new ArrayList<>();
        if (allowedValues != null) {
            allowedValues.stream().forEach(newAllowedValues::add);
        }

        return ObjectContext.buildFor(superType).withDefaultValue(defaultValue).withAllowedValues(
                newAllowedValues).build();
    }

    public GenericType getType() {
        return type;
    }

    public T getDefaultValue() {
        return defaultValue;
    }

    public List getAllowedValues() {
        return allowedValues;
    }

    public boolean isRequired() {
        return required;
    }

    public SizeConstraints getSizeConstraints() {
        return sizeConstraints;
    }

    public ValueConstraints getValueConstraints() {
        return valueConstraints;
    }

    public PropertyType getPropertyType() {
        return propertyType;
    }

    public PropertySubType getPropertySubType() {
        return propertySubType;
    }

    public Class getSchemaGenerator() {
        return schemaGenerator;
    }

    public ObjectContext forSuperType() {
        GenericType superType = type.getSuperType();
        if (superType != null) {
            return buildObjectContextForSuper(superType, allowedValues, defaultValue);
        } else {
            return null;
        }
    }

    public ObjectContext getContained() {
        final GenericType containedType = type.getContainedType();
        return ObjectContext.buildFor(containedType).build();
    }

    @SuppressWarnings("unchecked")
    public  ObjectContext forField(Field field) {
        final GenericType fieldType = GenericType.of(GenericTypeReflector.getExactFieldType(
                field, type.getType()), (Class) field.getType());
        final Builder builder = ObjectContext.buildFor(fieldType);

        if (defaultValue != null) {
            try {
                builder.withDefaultValue((U) field.get(defaultValue));
            } catch (IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }

        if (allowedValues != null && !fieldType.getRawType().isPrimitive()) {
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            List newAllowedValues = new ArrayList<>();
            allowedValues.stream().forEach(x -> addToAllowedValues(field, newAllowedValues, x));
            builder.withAllowedValues(newAllowedValues);
        }

        if (isRequired(field)) {
            builder.setRequired();
        }

        determineConstraints(Size.class, field, SizeConstraints::new).ifPresent(builder::withSizeConstraints);
        builder.withValueConstraints(new ValueConstraints(
                determineConstraints(Max.class, field, Max::value),
                determineConstraints(Min.class, field, Min::value)));

        final PropertySchema schemaGenerator = field.getAnnotation(PropertySchema.class);
        if (schemaGenerator != null) {
            builder.withSchemaGenerator(schemaGenerator.schemaGenerator());
        }

        return builder.build();
    }

    private boolean isRequired(Field field) {
        if (field.isAnnotationPresent(NotNull.class)) {
            return true;
        }
        Annotation[] annotations = field.getAnnotations();
        for (Annotation annotation : annotations) {
            Class annotationType = annotation.annotationType();
            if (annotationType.isAnnotationPresent(Constraint.class) && annotationType
                    .isAnnotationPresent(NotNull.class)) {
                return true;
            }
        }
        return false;
    }

    private  Optional determineConstraints(Class clazz, Field field, Function callback) {
        C constraint = field.getAnnotation(clazz);
        if (constraint != null) {
            return Optional.of(callback.apply(constraint));
        }
        for (Annotation annotation : field.getAnnotations()) {
            Class annotationType = annotation.annotationType();
            if (annotationType.isAnnotationPresent(Constraint.class) && annotationType.isAnnotationPresent(clazz)) {
                return Optional.of(callback.apply(annotationType.getAnnotation(clazz)));
            }
        }
        return Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private  void addToAllowedValues(Field field, List newAllowedValues, T x) {
        try {
            if (x != null) {
                newAllowedValues.add((U) field.get(x));
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    public boolean isApplicable(Field field, SchemaPropertyContext context) {
        return !Modifier.isStatic(field.getModifiers()) && //
                !field.isSynthetic() && //
                !IGNORE_ANNOTATIONS.stream().anyMatch(a -> field.getAnnotation(a) != null) && //
                isApplicableFor(field, context) && //
                isApplicableForPathParam(field);
    }

    private boolean isApplicableForPathParam(Field field) {
        PathParam pathParamAnnotation = field.getAnnotation(PathParam.class);
        if (pathParamAnnotation == null) {
            return true;
        }
        if (currentValue == null) {
            return false;
        }
        try {
            field.setAccessible(true);
            return (field.get(currentValue) == null);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            return false;
        }
    }

    private boolean isApplicableFor(Field field, SchemaPropertyContext context) {
        return field.getDeclaringClass().equals(ObjectWithSchema.class)
                || context.isFieldApplicable(field);
    }

    public Class getRawType() {
        return type.getRawType();
    }

    public static class Builder {

        private final GenericType type;

        private T defaultValue;

        private List allowedValues;

        private T currentValue;

        private boolean required;

        private SizeConstraints sizeConstraints = SizeConstraints.empty();

        private ValueConstraints valueConstraints = ValueConstraints.empty();

        private Class schemaGenerator;

        private Builder(GenericType type) {
            this.type = type;
        }

        public Builder withDefaultValue(T defaultValue) {
            this.defaultValue = defaultValue;
            return this;
        }

        public Builder withAllowedValues(List allowedValues) {
            this.allowedValues = allowedValues;
            return this;
        }

        public Builder withAllowedValue(T allowedValue) {
            this.allowedValues = ImmutableList.of(allowedValue);
            return this;
        }

        public Builder withCurrentValue(T currentValue) {
            this.currentValue = currentValue;
            return this;
        }

        public ObjectContext build() {
            return new ObjectContext<>(type, defaultValue, allowedValues, required, sizeConstraints, valueConstraints,
                    schemaGenerator, currentValue);
        }

        Builder setRequired() {
            required = true;
            return this;
        }

        Builder withSizeConstraints(SizeConstraints sizeConstraints) {
            this.sizeConstraints = sizeConstraints;
            return this;
        }

        Builder withValueConstraints(ValueConstraints valueConstraints) {
            this.valueConstraints = valueConstraints;
            return this;
        }

        Builder withSchemaGenerator(Class schemaGenerator) {
            this.schemaGenerator = schemaGenerator;
            return this;
        }
    }
}