com.github.cherimojava.data.mongo.entity.ParameterProperty.java Source code

Java tutorial

Introduction

Here is the source code for com.github.cherimojava.data.mongo.entity.ParameterProperty.java

Source

/**
 * Copyright (C) 2013 cherimojava (http://github.com/cherimojava/cherimodata) 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 com.github.cherimojava.data.mongo.entity;

import static com.github.cherimojava.data.mongo.entity.EntityUtils.getAdderFromGetter;
import static com.github.cherimojava.data.mongo.entity.EntityUtils.getSetterFromGetter;
import static com.github.cherimojava.data.mongo.entity.EntityUtils.isAssignableFromClass;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.validation.metadata.BeanDescriptor;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.bson.types.ObjectId;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.cherimojava.data.mongo.entity.annotation.Computed;
import com.github.cherimojava.data.mongo.entity.annotation.Final;
import com.github.cherimojava.data.mongo.entity.annotation.Reference;
import com.github.cherimojava.data.mongo.entity.annotation.Transient;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Primitives;

/**
 * Contains information for a parameter like it's mongodb name, if it's transient, etc. This class is immutable, use
 * {@link ParameterProperty.Builder} to create a new Instance of ParameterProperty
 *
 * @author pknobel
 * @since 1.0.0
 */
public final class ParameterProperty {
    private final String mongoName;

    private final String pojoName;

    private final boolean constraints;

    private final Class<?> type;

    private final Class<?> genericType;

    private final Validator validator;

    private final Class<? extends Entity> declaringClass;

    private final boolean tranzient;

    private final Computer computer;

    private final ReferenceLoadingTime referenceLoadingTime;

    private final ReferenceType referenceType;

    private final Map<MethodType, Boolean> typeReturnMap;

    private final boolean finl;

    private final boolean isPrimitiveType;

    ParameterProperty(Builder builder) {
        checkNotNull(builder.type, "type cannot be null");
        checkNotNull(builder.validator);
        checkArgument(StringUtils.isNotEmpty(builder.pojoName), "pojo name cannot be null or empty string");
        checkArgument(StringUtils.isNotEmpty(builder.mongoName), "mongo name cannot be null or empty string");

        typeReturnMap = Collections.unmodifiableMap(builder.typeReturnMap);
        type = Primitives.wrap(builder.type);
        isPrimitiveType = builder.type.isPrimitive();
        genericType = builder.genericType;
        pojoName = builder.pojoName;
        mongoName = builder.mongoName;
        constraints = builder.constraints;
        validator = builder.validator;
        declaringClass = builder.declaringClass;
        tranzient = builder.tranzient;
        finl = builder.finl;
        computer = builder.computer;
        referenceLoadingTime = builder.referenceLoadingTime;
        referenceType = builder.referenceType;
    }

    /**
     * gets the name of this parameter which is used on mongodb side
     */
    public String getMongoName() {
        return mongoName;
    }

    /**
     * gets the name of this parameter within the pojo, returned name doesn't contain set/get/add or any other prefix
     */
    public String getPojoName() {
        return pojoName;
    }

    /**
     * returns if this method allows for fluent API access or not. Returns null if the specified method isn't existent
     * for this property
     *
     * @param type add or get method to check
     */
    public Boolean isFluent(MethodType type) {
        return typeReturnMap.get(type);
    }

    /**
     * returns the type of the property this instance represents. In case the property is of primitive nature its
     * wrapper type will be returned
     */
    public Class<?> getType() {
        return type;
    }

    /**
     * returns if this ParameterProperty has Constraints, which need to be validated on set/save
     */
    public boolean hasConstraints() {
        return constraints;
    }

    /**
     * returns if this property isn't intended to be stored to MongoDB
     *
     * @return if this property is transient or not
     */
    public boolean isTransient() {
        return tranzient;
    }

    /**
     * returns if this property is computed by a computer
     *
     * @return if this property is computed
     */
    public boolean isComputed() {
        return computer != null;
    }

    /**
     * returns if this property is final or not. Meaning that after the value was peristed once it's not meant to be
     * changed again
     * 
     * @return
     */
    public boolean isFinal() {
        return finl;
    }

    /**
     * Returns if this property is only referenced and the actual property is stored in a separate collection. Only
     * valid if the property is a entity subtype
     *
     * @return if this property is a reference to a different entity
     */
    public boolean isReference() {
        return ReferenceType.NONE != referenceType;
    }

    /**
     * Returns if this property is supposed to be stored as Mongo style DBRef. Is true only when the property is a
     * reference and value DBRef is true
     * 
     * @return
     */
    public boolean isDBRef() {
        return ReferenceType.DBREF == referenceType;
    }

    /**
     * Returns if the given property is representing an Entity reference, which is lazily loaded
     *
     * @return if this property is loaded on first access to it
     */
    public boolean isLazyLoaded() {
        return ReferenceLoadingTime.LAZY == referenceLoadingTime;
    }

    /**
     * gets the computer for this property
     *
     * @return computer of this property if the property is computer, null otherwise
     */
    public Computer getComputer() {
        return computer;
    }

    /**
     * returns if the property is a collection, meaning the return type needs to be enriched with the actual class the
     * return type contains.
     *
     * @return true if the type is a collection, false otherwise
     */
    public boolean isCollection() {
        return genericType != null;
    }

    /**
     * returns the contained type if {@link #isCollection()} is returning true. Otherwise null is returned
     *
     * @return
     */
    public Class<?> getGenericType() {
        return genericType;
    }

    /**
     * returns if the property is of primitive type or not.
     * 
     * @return true if the original type is primitive otherwise false
     */
    public boolean isPrimitiveType() {
        return isPrimitiveType;
    }

    /**
     * validates the given value if it matches the defined constraints for this property. Throws
     * ConstraintViolationException if the value doesn't comply with the declared Constraints
     *
     * @param value property value to check for validity
     */
    public void validate(Object value) {
        if (value != null) {
            if (!type.isAssignableFrom(Primitives.wrap(value.getClass()))) {
                throw new ClassCastException(format("Can't cast from '%s' to '%s'",
                        value.getClass().getCanonicalName(), type.getCanonicalName()));
            }
        }
        if (hasConstraints()) {
            Set<? extends ConstraintViolation<? extends Entity>> violations = validator
                    .validateValue(declaringClass, pojoName, value, Entity.Special.class);
            if (!violations.isEmpty()) {
                StringBuilder msg = new StringBuilder().append("Found errors while validating property ")
                        .append(declaringClass.getCanonicalName()).append("#").append(pojoName).append(":\n");
                for (ConstraintViolation violation : violations) {
                    msg.append("*").append(violation.getMessage()).append("\n");
                }
                throw new ConstraintViolationException(msg.toString(), violations);
            }
        }
    }

    /**
     * Builder to create a new {@link ParameterProperty}
     *
     * @author philnate
     */
    static class Builder {
        private static final Logger LOG = LoggerFactory.getLogger(Builder.class);

        private String mongoName;

        private String pojoName;

        private Class<?> type;

        private Class<?> genericType;

        private boolean constraints = false;

        private Validator validator;

        private Class<? extends Entity> declaringClass;

        private boolean tranzient;

        private boolean finl;

        private Computer computer;

        private ReferenceLoadingTime referenceLoadingTime;

        private ReferenceType referenceType = ReferenceType.NONE;

        private Map<MethodType, Boolean> typeReturnMap = Maps.newHashMap();

        Builder setTransient(boolean tranzient) {
            this.tranzient = tranzient;
            return this;
        }

        Builder setMongoName(String mongoName) {
            this.mongoName = mongoName;
            return this;
        }

        Builder setPojoName(String pojoName) {
            this.pojoName = pojoName;
            return this;
        }

        Builder setFluent(MethodType method, boolean fluent) {
            typeReturnMap.put(method, fluent);
            return this;
        }

        Builder setType(Class<?> type) {
            this.type = type;
            return this;
        }

        Builder setGenericType(Class<?> genericType) {
            this.genericType = genericType;
            return this;
        }

        Builder hasConstraints(boolean constraints) {
            this.constraints = constraints;
            return this;
        }

        Builder setValidator(Validator validator) {
            this.validator = validator;
            return this;
        }

        Builder setDeclaringClass(Class<? extends Entity> declaringClass) {
            this.declaringClass = declaringClass;
            return this;
        }

        Builder setComputer(Computer computer) {
            this.computer = computer;
            return this;
        }

        Builder setReferenceLoadingTime(ReferenceLoadingTime reference) {
            this.referenceLoadingTime = reference;
            return this;
        }

        Builder setReferenceType(ReferenceType type) {
            this.referenceType = type;
            return this;
        }

        Builder setFinal(boolean finl) {
            this.finl = finl;
            return this;
        }

        ParameterProperty build() {
            return new ParameterProperty(this);
        }

        /**
         * creates a new {@link ParameterProperty} based on the attributes from the given get Method
         *
         * @param m to create ParameterProperty from
         * @return ParameterProperty containing the information from the given method
         */
        @SuppressWarnings("unchecked")
        static ParameterProperty buildFrom(Method m, Validator validator) {
            Class<? extends Entity> declaringClass = (Class<? extends Entity>) m.getDeclaringClass();
            BeanDescriptor bdesc = validator.getConstraintsForClass(declaringClass);
            Computer computer = null;
            Computed c = m.getAnnotation(Computed.class);
            Class<?> returnType = m.getReturnType();
            Builder builder = new Builder();
            if (c != null) {
                try {
                    computer = c.value().newInstance();
                } catch (Exception e) {
                    throw Throwables.propagate(e);
                }
            } else {
                // only if this is not a computed property we have a setter for it
                builder.setFluent(MethodType.SETTER, isAssignableFromClass(getSetterFromGetter(m)));
            }
            if (Collection.class.isAssignableFrom(m.getReturnType())) {
                try {
                    // only if we have an adder enabled Property type check for this
                    builder.setFluent(MethodType.ADDER, isAssignableFromClass(getAdderFromGetter(m)));
                } catch (IllegalArgumentException e) {
                    LOG.info("No Adder method declared for {} in Entity {}", m.getName(),
                            m.getDeclaringClass().getName());
                }
            }
            // check if something marks this property as final (no modification after initial save
            boolean finl = m.isAnnotationPresent(Final.class);
            if (!finl) {
                for (Annotation a : Lists.newArrayList(m.getAnnotations())) {
                    if (a.annotationType().isAnnotationPresent(Final.class)) {
                        finl = true;
                        break;
                    }
                }
            }
            if (finl) {
                // check that final is only on primitives
                checkArgument(
                        ClassUtils.isPrimitiveOrWrapper(returnType) || String.class.equals(returnType)
                                || ObjectId.class.equals(returnType) || DateTime.class.equals(returnType),
                        "Final is only supported for primitive types, jodatime DateTime and bson ObjectId but was %s",
                        returnType);
            }

            String mongoName = EntityUtils.getMongoNameFromMethod(m);
            checkArgument(!mongoName.startsWith("_") || Entity.ID.equals(mongoName),
                    "Property can't start with '_' as this is reserved, but got '%s'", mongoName);

            builder.setType( /** dont wrap primitives */
                    returnType).setPojoName(EntityUtils.getPojoNameFromMethod(m)).setMongoName(mongoName)
                    .hasConstraints(bdesc.getConstraintsForProperty(EntityUtils.getPojoNameFromMethod(m)) != null)
                    .setValidator(validator).setDeclaringClass(declaringClass)
                    .setTransient(m.isAnnotationPresent(Transient.class)).setComputer(computer).setFinal(finl);
            if (Collection.class.isAssignableFrom(m.getReturnType())) {
                checkArgument(m.getGenericReturnType().getClass() != Class.class, "Collections need to be generic");
                Type type = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments()[0];
                if (TypeVariable.class.isAssignableFrom(type.getClass())) {
                    // right now I dont know how to provide actual type at runtime...
                    builder.setGenericType(Entity.class);
                } else {
                    builder.setGenericType((Class) type);
                }
            }
            if (m.isAnnotationPresent(Reference.class)) {
                checkArgument(EntityUtils.isValidReferenceClass(m),
                        "Reference annotation can only be used for Entity types but was {}", m.getReturnType());
                builder.setReferenceLoadingTime(m.getAnnotation(Reference.class).lazy() ? ReferenceLoadingTime.LAZY
                        : ReferenceLoadingTime.IMMEDIATE);
                builder.setReferenceType(
                        m.getAnnotation(Reference.class).asDBRef() ? ReferenceType.DBREF : ReferenceType.SIMPLE);
            } else {
                builder.setReferenceType(ReferenceType.NONE);
            }
            return builder.build();
        }
    }

    /**
     * Enumeration about Reference LoadingTimes. This is information is needed for Referenced Entities loading
     */
    private static enum ReferenceLoadingTime {
        IMMEDIATE, // value is reference but immediately loaded
        LAZY// value is lazy loaded reference
    }

    /**
     * type of references the framework can handle
     */
    public static enum ReferenceType {
        NONE, // no reference
        SIMPLE, // only the id is stored
        DBREF, // Mongo DBRef style
    }

    /**
     * Information about the method being described
     */
    static enum MethodType {
        ADDER, SETTER
    }
}