com.boylesoftware.web.impl.UserInputControllerMethodArgHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.boylesoftware.web.impl.UserInputControllerMethodArgHandler.java

Source

/*
 * Copyright 2013 Boyle Software, Inc.
 *
 * 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.boylesoftware.web.impl;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.boylesoftware.web.api.Attributes;
import com.boylesoftware.web.api.UserInputErrors;
import com.boylesoftware.web.input.Bind;
import com.boylesoftware.web.input.Binder;
import com.boylesoftware.web.input.BindingException;
import com.boylesoftware.web.input.NoTrim;
import com.boylesoftware.web.input.binders.BooleanBinder;
import com.boylesoftware.web.input.binders.EnumBinder;
import com.boylesoftware.web.input.binders.IntegerBinder;
import com.boylesoftware.web.input.binders.StringBinder;
import com.boylesoftware.web.input.validation.DynamicValidationGroups;
import com.boylesoftware.web.spi.RouterRequest;
import com.boylesoftware.web.spi.UserInputHandler;
import com.boylesoftware.web.util.StringUtils;
import com.boylesoftware.web.util.pool.AbstractPoolable;
import com.boylesoftware.web.util.pool.FastPool;
import com.boylesoftware.web.util.pool.PoolableObjectFactory;

/**
 * Handler for user input bean controller method arguments.
 *
 * @author Lev Himmelfarb
 */
class UserInputControllerMethodArgHandler implements UserInputHandler {

    /**
     * Name of request attribute used to store the poolable user input bean
     * wrapper.
     */
    private static final String POOLED_OBJ_ATTNAME = (UserInputControllerMethodArgHandler.class).getName()
            + ".POOLED_OBJ";

    /**
     * Poolable wrapper for user input bean.
     */
    private static final class PoolableUserInput extends AbstractPoolable {

        /**
         * The user input bean.
         */
        private final Object bean;

        /**
         * Create new wrapper.
         *
         * @param pool The pool.
         * @param pooledObjectId Pooled object id.
         * @param bean User input bean.
         */
        PoolableUserInput(final FastPool<PoolableUserInput> pool, final int pooledObjectId, final Object bean) {
            super(pool, pooledObjectId);

            this.bean = bean;
        }

        /**
         * Get wrapped user input bean.
         *
         * @return The bean.
         */
        Object getBean() {

            return this.bean;
        }
    }

    /**
     * User input bean field descriptor.
     */
    private static final class FieldDesc {

        /**
         * Bean property descriptor.
         */
        private final PropertyDescriptor propDesc;

        /**
         * Tells if the field has a {@link NoTrim} annotation.
         */
        private final boolean noTrim;

        /**
         * Field value binder.
         */
        private final Binder binder;

        /**
         * Format for the binder.
         */
        private final String format;

        /**
         * Error message if binder fails.
         */
        private final String errorMessage;

        /**
         * Create new descriptor.
         *
         * @param propDesc Bean property descriptor.
         * @param noTrim {@code true} if the field has a {@link NoTrim}
         * annotation.
         * @param binder Field value binder.
         * @param format Format for the binder.
         * @param errorMessage Error message if bider fails.
         */
        FieldDesc(final PropertyDescriptor propDesc, final boolean noTrim, final Binder binder, final String format,
                final String errorMessage) {

            this.propDesc = propDesc;
            this.noTrim = noTrim;
            this.binder = binder;
            this.format = format;
            this.errorMessage = errorMessage;
        }

        /**
         * Get bean property descriptor.
         *
         * @return Bean property descriptor.
         */
        PropertyDescriptor getPropDesc() {

            return this.propDesc;
        }

        /**
         * Tell if the field has a {@link NoTrim} annotation.
         *
         * @return {@code true} if the field has a {@link NoTrim} annotation.
         */
        boolean isNoTrim() {

            return this.noTrim;
        }

        /**
         * Get the field value bider.
         *
         * @return The binder.
         */
        Binder getBinder() {

            return this.binder;
        }

        /**
         * Get format for the binder.
         *
         * @return The format.
         */
        String getFormat() {

            return this.format;
        }

        /**
         * Get error message if binder fails.
         *
         * @return The error message template.
         */
        String getErrorMessage() {

            return this.errorMessage;
        }
    }

    /**
     * The log.
     */
    private final Log log = LogFactory.getLog(this.getClass());

    /**
     * Validator factory.
     */
    private final ValidatorFactory validatorFactory;

    /**
     * User input bean class.
     */
    private final Class<?> beanClass;

    /**
     * Validation groups.
     */
    private final Class<?>[] validationGroups;

    /**
     * Descriptors of the user input bean field.
     */
    private final FieldDesc[] beanFields;

    /**
     * Bean pool.
     */
    private final FastPool<PoolableUserInput> beanPool;

    /**
     * Create new handler.
     *
     * @param validatorFactory Validator factory.
     * @param beanClass User input bean class.
     * @param validationGroups Validation groups to apply during bean
     * validation, or empty array to use the default group.
     *
     * @throws UnavailableException If an error happens.
     */
    UserInputControllerMethodArgHandler(final ValidatorFactory validatorFactory, final Class<?> beanClass,
            final Class<?>[] validationGroups) throws UnavailableException {

        this.validatorFactory = validatorFactory;

        this.beanClass = beanClass;
        this.validationGroups = validationGroups;

        try {
            final BeanInfo beanInfo = Introspector.getBeanInfo(this.beanClass);
            final PropertyDescriptor[] propDescs = beanInfo.getPropertyDescriptors();
            final List<FieldDesc> beanFields = new ArrayList<>();
            for (final PropertyDescriptor propDesc : propDescs) {
                final String propName = propDesc.getName();
                final Class<?> propType = propDesc.getPropertyType();
                final Method propGetter = propDesc.getReadMethod();
                final Method propSetter = propDesc.getWriteMethod();

                if ((propGetter == null) || (propSetter == null))
                    continue;

                Field propField = null;
                for (Class<?> c = this.beanClass; !c.equals(Object.class); c = c.getSuperclass()) {
                    try {
                        propField = c.getDeclaredField(propName);
                        break;
                    } catch (final NoSuchFieldException e) {
                        // nothing, continue the loop
                    }
                }
                final boolean noTrim = (((propField != null) && propField.isAnnotationPresent(NoTrim.class))
                        || (propGetter.isAnnotationPresent(NoTrim.class)));

                Class<? extends Binder> binderClass = null;
                String format = null;
                String errorMessage = Bind.DEFAULT_MESSAGE;
                Bind bindAnno = null;
                if (propField != null)
                    bindAnno = propField.getAnnotation(Bind.class);
                if (bindAnno == null)
                    bindAnno = propGetter.getAnnotation(Bind.class);
                if (bindAnno != null) {
                    binderClass = bindAnno.binder();
                    format = bindAnno.format();
                    errorMessage = bindAnno.message();
                }
                if (binderClass == null) {
                    if ((String.class).isAssignableFrom(propType))
                        binderClass = StringBinder.class;
                    else if ((Boolean.class).isAssignableFrom(propType) || propType.equals(Boolean.TYPE))
                        binderClass = BooleanBinder.class;
                    else if ((Integer.class).isAssignableFrom(propType) || propType.equals(Integer.TYPE))
                        binderClass = IntegerBinder.class;
                    else if (propType.isEnum())
                        binderClass = EnumBinder.class;
                    else // TODO: add more standard binders
                        throw new UnavailableException(
                                "Unsupported user input bean field type " + propType.getName() + ".");
                }

                beanFields.add(new FieldDesc(propDesc, noTrim, binderClass.newInstance(), format, errorMessage));
            }
            this.beanFields = beanFields.toArray(new FieldDesc[beanFields.size()]);
        } catch (final IntrospectionException e) {
            this.log.error("error introspecting user input bean", e);
            throw new UnavailableException("Specified user input bean" + " class could not be introspected.");
        } catch (final IllegalAccessException | InstantiationException e) {
            this.log.error("error instatiating binder", e);
            throw new UnavailableException("Used user input bean field binder" + " could not be instantiated.");
        }

        this.beanPool = new FastPool<>(new PoolableObjectFactory<PoolableUserInput>() {

            @Override
            public PoolableUserInput makeNew(final FastPool<PoolableUserInput> pool, final int pooledObjectId) {

                try {
                    return new PoolableUserInput(pool, pooledObjectId, beanClass.newInstance());
                } catch (final InstantiationException | IllegalAccessException e) {
                    throw new RuntimeException("Error instatiating user input bean.", e);
                }
            }
        }, "UserInputBeansPool_" + beanClass.getSimpleName());
    }

    /* (non-Javadoc)
     * @see com.boylesoftware.web.spi.UserInputHandler#prepareUserInput(com.boylesoftware.web.spi.RouterRequest)
     */
    @Override
    public boolean prepareUserInput(final RouterRequest request) throws ServletException {

        boolean success = false;
        final PoolableUserInput pooledUserInput = this.beanPool.getSync();
        try {
            request.setAttribute(POOLED_OBJ_ATTNAME, pooledUserInput);
            final Object bean = pooledUserInput.getBean();
            request.setAttribute(Attributes.USER_INPUT, bean);
            final UserInputErrors errors = request.getUserInputErrors();
            request.setAttribute(Attributes.USER_INPUT_ERRORS, errors);

            // bind the bean properties
            final int numProps = this.beanFields.length;
            for (int i = 0; i < numProps; i++) {
                final FieldDesc fieldDesc = this.beanFields[i];
                final PropertyDescriptor propDesc = fieldDesc.getPropDesc();
                final String propName = propDesc.getName();
                final String propValStr = (fieldDesc.isNoTrim()
                        ? StringUtils.nullIfEmpty(request.getParameter(propName))
                        : StringUtils.trimToNull(request.getParameter(propName)));
                final Method propSetter = propDesc.getWriteMethod();
                try {
                    propSetter.invoke(bean, fieldDesc.getBinder().convert(request, propValStr,
                            fieldDesc.getFormat(), propDesc.getPropertyType()));
                } catch (final BindingException e) {
                    if (this.log.isDebugEnabled())
                        this.log.debug("binding error", e);
                    propSetter.invoke(bean, e.getDefaultValue());
                    errors.add(propName, fieldDesc.getErrorMessage());
                }
            }

            // validate the bean
            final Validator validator = this.validatorFactory.usingContext()
                    .messageInterpolator(request.getMessageInterpolator()).getValidator();
            Class<?>[] validationGroups = this.validationGroups;
            if ((this.validationGroups.length == 0) && (bean instanceof DynamicValidationGroups))
                validationGroups = ((DynamicValidationGroups) bean).getValidationGroups(request);
            final Set<ConstraintViolation<Object>> cvs = validator.validate(bean, validationGroups);
            final boolean valid = cvs.isEmpty();
            if (!valid) {
                for (final ConstraintViolation<Object> cv : cvs)
                    errors.add(cv.getPropertyPath().toString(), cv.getMessage());
            }

            success = true;

            return valid;

        } catch (final IllegalAccessException | InvocationTargetException e) {
            throw new ServletException("Error working with user input bean.", e);
        } finally {
            if (!success)
                pooledUserInput.recycle();
        }
    }

    /* (non-Javadoc)
     * @see com.boylesoftware.web.spi.ControllerMethodArgHandler#usesEntityManager()
     */
    @Override
    public boolean usesEntityManager() {

        return false;
    }

    /* (non-Javadoc)
     * @see com.boylesoftware.web.spi.ControllerMethodArgHandler#getArgValue(com.boylesoftware.web.spi.RouterRequest, javax.persistence.EntityManager)
     */
    @Override
    public Object getArgValue(final RouterRequest request, final EntityManager em) {

        return request.getAttribute(Attributes.USER_INPUT);
    }

    /* (non-Javadoc)
     * @see com.boylesoftware.web.spi.ControllerMethodArgHandler#onComplete(com.boylesoftware.web.spi.RouterRequest)
     */
    @Override
    public void onComplete(final RouterRequest request) {

        request.removeAttribute(Attributes.USER_INPUT);
        request.removeAttribute(Attributes.USER_INPUT_ERRORS);

        final PoolableUserInput pooledUserInput = (PoolableUserInput) request.getAttribute(POOLED_OBJ_ATTNAME);
        if (pooledUserInput != null)
            pooledUserInput.recycle();
    }
}