org.brekka.stillingar.spring.bpp.ConfigurationBeanPostProcessor.java Source code

Java tutorial

Introduction

Here is the source code for org.brekka.stillingar.spring.bpp.ConfigurationBeanPostProcessor.java

Source

/*
 * Copyright 2011 the original author or authors.
 *
 * 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 org.brekka.stillingar.spring.bpp;

import static java.lang.String.format;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.brekka.stillingar.api.ConfigurationException;
import org.brekka.stillingar.api.ConfigurationSource;
import org.brekka.stillingar.api.annotations.ConfigurationListener;
import org.brekka.stillingar.api.annotations.Configured;
import org.brekka.stillingar.core.ConfigurationService;
import org.brekka.stillingar.core.GroupChangeListener;
import org.brekka.stillingar.core.GroupConfigurationException;
import org.brekka.stillingar.core.SingleValueDefinition;
import org.brekka.stillingar.core.ValueChangeListener;
import org.brekka.stillingar.core.ValueDefinition;
import org.brekka.stillingar.core.ValueDefinitionGroup;
import org.brekka.stillingar.core.ValueListDefinition;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;

/**
 * Identifies and enhances Spring managed beans that are marked with the {@link Configured} annotation and contain
 * fields/methods that marked to be configured. Fields and setter methods would be marked with {@link Configured}, with
 * listener methods marked with {@link ConfigurationListener}.
 * 
 * If the {@link ConfigurationSource} passed to this post-processor is also an instance of {@link ConfigurationService}
 * then all configuration will be registered to receive updates from the configuration source.
 * 
 * @author Andrew Taylor
 */
public class ConfigurationBeanPostProcessor
        implements BeanPostProcessor, BeanFactoryAware, DisposableBean, Ordered {

    /**
     * Logger providing helpful output (if enabled).
     */
    private static final Log log = LogFactory.getLog(ConfigurationBeanPostProcessor.class);

    /**
     * The configuration source from which configuration values will be resolved, and potentially registered to receive
     * automatic updates.
     */
    private final ConfigurationSource configurationSource;

    /**
     * Name of the configuration source.
     */
    private final String name;

    /**
     * The list of registered groups
     */
    private final List<ValueDefinitionGroup> registeredValueGroups = new ArrayList<ValueDefinitionGroup>();

    /**
     * Will be used to identify whether a bean is singleton or not, and also to lookup beans for the
     * {@link ConfigurationListener} mechanism.
     */
    private BeanFactory beanFactory;

    /**
     * The class level annotation to identify beans that should be configured.
     */
    private Class<? extends Annotation> markerAnnotation = Configured.class;

    /**
     * A cache of the {@link ValueDefinitionGroup} assigned to a given type whose instances will not receive updates.
     * This is the case where a bean definition is non-singleton or the {@link ConfigurationSource} is immutable.
     */
    private Map<Class<?>, ValueDefinitionGroup> onceOnlyDefinitionCache = new HashMap<Class<?>, ValueDefinitionGroup>();

    /**
     * @param name
     *            the name of the configuration source
     * @param configurationSource
     *            The configuration source from which configuration values will be resolved, and potentially registered
     *            to receive automatic updates.
     */
    public ConfigurationBeanPostProcessor(String name, ConfigurationSource configurationSource) {
        this.name = name;
        this.configurationSource = configurationSource;
    }

    /**
     * Checks whether the bean is annotated with {@link Configured} (or other) and determines whether the bean needs to
     * be registered for updates or just configured one-time with the current configuration state.
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        if (hasMarkerAnnotation(beanClass)) {
            boolean singleton = beanFactory.isSingleton(beanName);
            try {
                if (singleton && configurationSource instanceof ConfigurationService) {
                    processWithUpdates(bean, beanName);
                } else {
                    processOnceOnly(bean, beanName);
                }
            } catch (GroupConfigurationException e) {
                throw new ConfigurationException(
                        String.format("Post processing bean '%s' using configuration source '%s'", beanName, name),
                        e);
            }
        }
        return bean;
    }

    /**
     * Unregister all registered beans from the configuration service.
     */
    @Override
    public void destroy() throws Exception {
        if (log.isInfoEnabled()) {
            log.info(String.format("Bean post processor preparing to destroy %d registered groups",
                    registeredValueGroups.size()));
        }
        synchronized (registeredValueGroups) {
            ConfigurationService configurationService = (ConfigurationService) configurationSource;
            for (ValueDefinitionGroup group : registeredValueGroups) {
                configurationService.unregister(group);
                if (log.isInfoEnabled()) {
                    log.info(String.format("Group '%s' has been unregistered from the configuration service",
                            group.getName()));
                }
            }
            registeredValueGroups.clear();
        }
    }

    /**
     * When configuration values are encountered, they will be retrieved and applied only. No updates will be performed.
     * Listeners will be called once to ensure we don't break their contract.
     * 
     * @param bean
     *            the bean being configured
     * @param beanName
     *            the name of the bean used in log messages etc.
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void processOnceOnly(Object bean, String beanName) {
        Class<? extends Object> targetClass = bean.getClass();
        ValueDefinitionGroup valueDefinitionGroup = onceOnlyDefinitionCache.get(targetClass);
        if (valueDefinitionGroup == null) {
            synchronized (onceOnlyDefinitionCache) {
                /*
                 * Capture the type of the target being configured. We don't want to use the bean itself as the
                 * definition will be reused for other instances, none of which should be updated.
                 */
                OnceOnlyTypeHolder target = new OnceOnlyTypeHolder(targetClass);
                valueDefinitionGroup = prepareValueGroup(beanName, target);
                // Cache the type.
                onceOnlyDefinitionCache.put(targetClass, valueDefinitionGroup);
            }
        }
        Collection<ValueDefinition<?, ?>> values = valueDefinitionGroup.getValues();
        for (ValueDefinition<?, ?> valueDefinition : values) {
            Object value;
            if (valueDefinition instanceof ValueListDefinition) {
                if (valueDefinition.getExpression() != null) {
                    value = configurationSource.retrieveList(valueDefinition.getExpression(),
                            valueDefinition.getType());
                } else {
                    value = configurationSource.retrieveList(valueDefinition.getType());
                }
            } else {
                if (valueDefinition.getExpression() != null) {
                    value = configurationSource.retrieve(valueDefinition.getExpression(),
                            valueDefinition.getType());
                } else {
                    value = configurationSource.retrieve(valueDefinition.getType());
                }
            }
            ValueChangeListener listener = valueDefinition.getChangeListener();
            if (listener instanceof PrototypeValueChangeListener) {
                PrototypeValueChangeListener pvcl = (PrototypeValueChangeListener) listener;
                pvcl.onChange(value, null, bean);
            } else {
                listener.onChange(value, null);
            }
        }
        GroupChangeListener changeListener = valueDefinitionGroup.getChangeListener();
        if (changeListener instanceof PrototypeGroupChangeListener) {
            /*
             * Note we are deliberately not obtaining the semaphore.
             */
            PrototypeGroupChangeListener pgcl = (PrototypeGroupChangeListener) changeListener;
            pgcl.onChange(configurationSource, bean);
        }
    }

    /**
     * Process configured field/method/listeners, also registering them for updates.
     * 
     * @param bean
     *            the bean being configured
     * @param beanName
     *            the name of the bean used in log messages etc.
     */
    protected void processWithUpdates(Object bean, String beanName) {
        ValueDefinitionGroup group = prepareValueGroup(beanName, bean);
        ConfigurationService configurationService = (ConfigurationService) configurationSource;
        configurationService.register(group, true);
        synchronized (registeredValueGroups) {
            if (log.isInfoEnabled()) {
                log.info(String.format("Registered the group '%s' with configuration service", group.getName()));
            }
            registeredValueGroups.add(group);
        }
    }

    /**
     * Prepare the {@link ValueDefinitionGroup} for the specified bean.
     * 
     * @param beanName
     *            the name of the bean used in log messages etc.
     * @param target
     *            the bean being configured
     * @return the value definition group
     */
    protected ValueDefinitionGroup prepareValueGroup(String beanName, Object target) {
        List<ValueDefinition<?, ?>> valueList = new ArrayList<ValueDefinition<?, ?>>();

        Class<? extends Object> beanClass = target.getClass();
        if (target instanceof OnceOnlyTypeHolder) {
            /*
             * The target bean is not available, just the type.
             */
            beanClass = ((OnceOnlyTypeHolder) target).get();
        }

        Class<?> inpectClass = beanClass;
        while (inpectClass != null) {
            Field[] declaredFields = inpectClass.getDeclaredFields();
            for (Field field : declaredFields) {
                processField(field, valueList, target);
            }
            inpectClass = inpectClass.getSuperclass();
        }

        PostUpdateChangeListener beanChangeListener = null;

        Method[] declaredMethods = beanClass.getDeclaredMethods();
        Arrays.sort(declaredMethods, new Comparator<Method>() {
            @Override
            public int compare(Method o1, Method o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        for (Method method : declaredMethods) {
            Configured configured = method.getAnnotation(Configured.class);

            ConfigurationListener configurationListener = method.getAnnotation(ConfigurationListener.class);
            if (configurationListener != null) {
                if (beanChangeListener != null) {
                    throw new ConfigurationException(format(
                            "Unable to create a configuration listener for the method '%s' on the bean '%s' (type '%s') "
                                    + "as it already contains a configuration listener on the method '%s'",
                            method.getName(), beanName, beanClass.getName(),
                            beanChangeListener.getMethod().getName()));
                }
                beanChangeListener = processListenerMethod(method, valueList, target);

            } else if (configured != null) {
                processSetterMethod(configured, method, valueList, target);
            }
        }
        ValueDefinitionGroup group = new ValueDefinitionGroup(beanName, valueList, beanChangeListener, target);
        return group;
    }

    /**
     * Encapsulates a field in a {@link ValueDefinition} so that it can be registered for updates.
     * 
     * @param field
     *            the field being processed
     * @param valueList
     *            the list of value definitions that the new {@link ValueDefinition} for this field will be added to.
     * @param bean
     *            the bean being configured.
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void processField(Field field, List<ValueDefinition<?, ?>> valueList, Object bean) {
        Configured annotation = field.getAnnotation(Configured.class);
        if (annotation != null) {
            Class type = field.getType();
            boolean list = false;
            ValueDefinition<Object, ?> value;
            if (type == List.class) {
                type = listType(field.getGenericType());
                FieldValueChangeListener<List<Object>> listener = new FieldValueChangeListener<List<Object>>(field,
                        bean, type, list);
                value = new ValueListDefinition<Object>(type, annotation.value(), listener);
            } else {
                FieldValueChangeListener<Object> listener = new FieldValueChangeListener<Object>(field, bean, type,
                        list);
                value = new SingleValueDefinition<Object>(type, annotation.value(), listener);
            }
            valueList.add(value);
        }
    }

    /**
     * Encapsulate a setter method in a {@link ValueDefinition} so that it can be registered for configuration updates.
     * 
     * @param configured
     *            the {@link Configured} attribute applied to the method.
     * @param method
     *            the method itself
     * @param valueList
     *            the list of value definitions that the new {@link ValueDefinition} for this field will be added to.
     * @param bean
     *            the bean being configured.
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void processSetterMethod(Configured configured, Method method, List<ValueDefinition<?, ?>> valueList,
            Object bean) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length != 1) {
            throw new ConfigurationException(format("The method '%s' does not appear to be a setter. "
                    + "A bean setter method should take only a single parameter.", method));
        }
        Class type = parameterTypes[0];
        boolean list = false;
        ValueDefinition<Object, ?> value;
        if (type == List.class) {
            Type[] genericParameterTypes = method.getGenericParameterTypes();
            type = listType(genericParameterTypes[0]);
            MethodValueChangeListener<List<Object>> listener = new MethodValueChangeListener<List<Object>>(method,
                    bean, type, list);
            value = new ValueListDefinition<Object>(type, configured.value(), listener);
        } else {
            MethodValueChangeListener<Object> listener = new MethodValueChangeListener<Object>(method, bean, type,
                    list);
            value = new SingleValueDefinition<Object>(type, configured.value(), listener);
        }
        valueList.add(value);
    }

    /**
     * Encapsulate the 'listener' method that will be invoked once all fields/setter methods have been updated. The
     * parameters of this method will be added as individual {@link ValueDefinition}'s to <code>valueList</code>.
     * 
     * @param method
     *            the method being encapsulated
     * @param valueList
     *            the list of value definitions that the new {@link ValueDefinition}s for this field will be added to.
     * @param bean
     *            the bean being configured.
     * @return the {@link PostUpdateChangeListener} that will invoke the listener method on configuration update.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected PostUpdateChangeListener processListenerMethod(Method method, List<ValueDefinition<?, ?>> valueList,
            Object bean) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        List<ParameterValueResolver> argList = new ArrayList<ParameterValueResolver>();
        for (int i = 0; i < parameterTypes.length; i++) {
            ParameterValueResolver arg = null;
            Annotation[] annotations = parameterAnnotations[i];
            Class type = parameterTypes[i];
            boolean list = false;
            if (type == List.class) {
                type = listType(genericParameterTypes[i]);
                list = true;
            }
            Qualifier qualifier = null;
            for (Annotation annotation : annotations) {
                if (annotation instanceof Configured) {
                    Configured paramConfigured = (Configured) annotation;
                    MethodParameterListener mpl = new MethodParameterListener();
                    ValueDefinition<Object, ?> value;
                    if (list) {
                        value = new ValueListDefinition<Object>(type, paramConfigured.value(), mpl);
                    } else {
                        value = new SingleValueDefinition<Object>(type, paramConfigured.value(), mpl);
                    }
                    valueList.add(value);
                    arg = mpl;
                    break;
                } else if (annotation instanceof Qualifier) {
                    qualifier = (Qualifier) annotation;
                }
            }
            if (arg == null) {
                if (qualifier != null) {
                    try {
                        beanFactory.getBean(qualifier.value(), type);
                        arg = new BeanReferenceResolver(beanFactory, qualifier, type);
                    } catch (NoSuchBeanDefinitionException e) {
                        throw new ConfigurationException(format(
                                "Listener method '%s' parameter %d is not marked as %s and no bean "
                                        + "definition could be found in the container with the qualifier '%s' and type '%s'.",
                                method.getName(), (i + 1), Configured.class.getSimpleName(), qualifier.value(),
                                type.getName()));
                    }
                } else {
                    try {
                        beanFactory.getBean(type);
                        arg = new BeanReferenceResolver(beanFactory, type);
                    } catch (NoSuchBeanDefinitionException e) {
                        throw new ConfigurationException(format(
                                "Listener method '%s' parameter %d is not marked as %s and no bean "
                                        + "definition could be found in the container with the type '%s'.",
                                method.getName(), (i + 1), Configured.class.getSimpleName(), type.getName()));
                    }
                }
            }
            argList.add(arg);
        }
        return new PostUpdateChangeListener(bean, method, argList);
    }

    /**
     * Determine if the bean class or any of its super types have a {@link Configured} (or other set by
     * <code>markerAnnotation</code>) annotation set.
     * 
     * @param beanClass
     *            the class to inspect
     * @return true if the class or any of its super types have the annotation. Note that this does not include
     *         interfaces as they do not support fields or concrete method definitions.
     */
    private boolean hasMarkerAnnotation(Class<?> beanClass) {
        boolean retVal = false;
        Class<?> inpectClass = beanClass;
        while (inpectClass != null) {
            if (inpectClass.getAnnotation(markerAnnotation) != null) {
                retVal = true;
                break;
            }
            inpectClass = inpectClass.getSuperclass();
        }
        return retVal;
    }

    /**
     * Simply returns the bean parameter.
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /**
     * Set the marker annotation that identifies the bean as being configured. By default set to {@link Configured}.
     * 
     * @param markerAnnotation
     *            the configured bean marker class.
     */
    public void setMarkerAnnotation(Class<? extends Annotation> markerAnnotation) {
        this.markerAnnotation = markerAnnotation;
    }

    /**
     * Set the bean factory
     */
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    /* (non-Javadoc)
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        return 10;
    }

    /**
     * Identifies the type of the parameterised list.
     * 
     * @param listType
     *            the list type to inspect
     * @return the list type or null if it is not parameterised.
     */
    @SuppressWarnings("rawtypes")
    private static Class<?> listType(Type listType) {
        Class<?> type;
        if (listType instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) listType;
            Type[] actualTypeArguments = pType.getActualTypeArguments();
            type = (Class) actualTypeArguments[0];
        } else {
            throw new ConfigurationException(String.format("Not a parameterised list type: '%s'", listType));
        }
        return type;
    }
}