Package org.vaadin.teemu.clara.binder

Source Code of org.vaadin.teemu.clara.binder.Binder

package org.vaadin.teemu.clara.binder;

import static org.vaadin.teemu.clara.util.ReflectionUtils.findMethods;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.vaadin.teemu.clara.Clara;
import org.vaadin.teemu.clara.binder.annotation.UiDataSource;
import org.vaadin.teemu.clara.binder.annotation.UiField;
import org.vaadin.teemu.clara.binder.annotation.UiHandler;
import org.vaadin.teemu.clara.util.MethodComparator;
import org.vaadin.teemu.clara.util.ReflectionUtils.ParamCount;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.ui.Component;

public class Binder {

    protected Logger getLogger() {
        return Logger.getLogger(Binder.class.getName());
    }

    /**
     * Binds fields and methods of the given {@code controller} instance to
     * {@link Component}s found in the given {@code componentRoot} component
     * hierarchy. The binding is defined by the annotations {@link UiField},
     * {@link UiHandler} and {@link UiDataSource}.
     *
     * @param componentRoot
     * @param controller
     *
     * @throws BinderException
     *             if an error is encountered during the binding.
     *
     * @see UiField
     * @see UiHandler
     * @see UiDataSource
     */
    public void bind(Component componentRoot, Object controller) {
        if (controller == null) {
            return;
        }

        bindFields(componentRoot, controller, controller.getClass());
        bindMethods(componentRoot, controller);
    }

    /**
     * Returns a {@link Map} from {@link String} id to {@link Component} of all
     * controller fields decorated with the {@link UiField} annotation that
     * already have a {@link Component} reference assigned. If the given
     * controller is {@code null}, an empty {@link Map} is returned.
     *
     * @param controller
     *
     * @see UiField
     */
    public Map<String, Component> getAlreadyAssignedFields(Object controller) {
        if (controller == null) {
            return Collections.emptyMap();
        }

        Map<String, Component> assignedFields = new HashMap<String, Component>();
        Field[] fields = controller.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(UiField.class)) {
                try {
                    field.setAccessible(true);
                    Object value = field.get(controller);
                    if (value instanceof Component) {
                        assignedFields.put(extractComponentId(field),
                                (Component) value);
                    }
                } catch (IllegalAccessException e) {
                    getLogger()
                            .log(Level.WARNING,
                                    "Exception while accessing controller object fields.",
                                    e);
                }
            }
        }
        return assignedFields;
    }

    private void bindFields(Component componentRoot, Object controller,
            Class<?> clazz) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(UiField.class)) {
                bindField(componentRoot, controller, field);
            }
        }
        Class<?> superclass = clazz.getSuperclass();
        if (superclass != null) {
            bindFields(componentRoot, controller, superclass);
        }
    }

    private void bindMethods(Component componentRoot, Object controller) {
        Method[] methods = controller.getClass().getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(UiDataSource.class)) {
                bindDataSource(componentRoot, controller, method);
            }

            if (method.isAnnotationPresent(UiHandler.class)) {
                bindEventHandler(componentRoot, controller, method);
            }
        }
    }

    private void bindField(Component componentRoot, Object controller,
            Field field) {
        String componentId = extractComponentId(field);
        Component component = tryToFindComponentById(componentRoot, componentId);

        try {
            field.setAccessible(true);
            if (field.get(controller) == null) {
                field.set(controller, component);
            }
        } catch (IllegalArgumentException e) {
            throw new BinderException(e);
        } catch (IllegalAccessException e) {
            throw new BinderException(e);
        }
    }

    /**
     * Returns id of the {@link Component} that the given {@link Field} should
     * be bound to. Assumes that the given {@code field} has {@link UiField}
     * annotation. This should be checked before calling this method.
     *
     * @return id of the component to bind the field.
     */
    private String extractComponentId(Field field) {
        // Try the id from UiField annotation.
        UiField annotation = field.getAnnotation(UiField.class);
        String componentId = annotation.value();

        if (componentId.length() == 0) {
            // Default to the field name instead of annotated id.
            componentId = field.getName();
        }
        return componentId;
    }

    /**
     * Expects that the given {@link Method} is annotated with {@link UiHandler}
     * annotation.
     *
     * @param componentRoot
     * @param controller
     * @param method
     */
    private void bindEventHandler(Component componentRoot, Object controller,
            Method method) {
        String componentId = method.getAnnotation(UiHandler.class).value();
        Component component = tryToFindComponentById(componentRoot, componentId);

        Class<?> eventType = (method.getParameterTypes().length > 0 ? method
                .getParameterTypes()[0] : null);
        if (eventType == null) {
            throw new BinderException(
                    "Couldn't figure out event type for method " + method + ".");
        }

        Method addListenerMethod = getAddListenerMethod(component.getClass(),
                eventType);
        if (addListenerMethod != null) {
            try {
                Object listener = createListenerProxy(
                        addListenerMethod.getParameterTypes()[0], eventType,
                        method, controller);
                addListenerMethod.invoke(component, listener);
            } catch (IllegalAccessException e) {
                throw new BinderException(e);
            } catch (InvocationTargetException e) {
                throw new BinderException(e);
            }
        }
    }

    private Object createListenerProxy(Class<?> listenerClass,
            final Class<?> eventClass, final Method listenerMethod,
            final Object controller) {
        Object proxy = Proxy.newProxyInstance(listenerClass.getClassLoader(),
                new Class<?>[] { listenerClass }, new InvocationHandler() {

                    @Override
                    public Object invoke(Object proxy, Method method,
                            Object[] args) throws Throwable {

                        if (args != null
                                && args.length > 0
                                && eventClass.isAssignableFrom(args[0]
                                        .getClass())) {
                            getLogger().fine(
                                    String.format(
                                            "Forwarding method call %s -> %s.",
                                            method.getName(),
                                            listenerMethod.getName()));
                            return listenerMethod.invoke(controller, args);
                        }
                        getLogger()
                                .fine(String.format(
                                        "Forwarding method call %s to %s.",
                                        method.getName(), controller.getClass()));
                        return method.invoke(controller, args);
                    }

                });
        getLogger().fine(
                String.format("Created a proxy for %s.", listenerClass));
        return proxy;
    }

    private Method getAddListenerMethod(
            Class<? extends Component> componentClass, Class<?> eventClass) {
        List<Method> addListenerCandidates = findMethods(componentClass,
                "add(.*)Listener", ParamCount.constant(1));
        Collections.sort(addListenerCandidates, new MethodComparator());

        for (Method addListenerCandidate : addListenerCandidates) {
            // Check if this method accepts correct type of listeners.
            Class<?> listenerInterface = addListenerCandidate
                    .getParameterTypes()[0];

            if (findMethods(listenerInterface, ".*", eventClass).size() == 1) {
                // There exist a single method in the listener interface that
                // accepts our eventClass as its sole parameter -> our candidate
                // is accepted.
                return addListenerCandidate;
            }
        }
        return null;
    }

    /**
     * Expects that the given {@link Method} is annotated with
     * {@link UiDataSource} annotation.
     *
     * @param componentRoot
     * @param controller
     * @param method
     */
    private void bindDataSource(Component componentRoot, Object controller,
            Method method) {
        String componentId = method.getAnnotation(UiDataSource.class).value();
        Component component = tryToFindComponentById(componentRoot, componentId);
        Class<?> dataSourceClass = method.getReturnType();

        try {
            // Vaadin data model consists of Property/Item/Container
            // objects and each of them have a Viewer interface.
            if (isContainer(dataSourceClass)
                    && component instanceof Container.Viewer) {
                ((Container.Viewer) component)
                        .setContainerDataSource((Container) method
                                .invoke(controller));
            } else if (isProperty(dataSourceClass)
                    && component instanceof Property.Viewer) {
                ((Property.Viewer) component)
                        .setPropertyDataSource((Property<?>) method
                                .invoke(controller));
            } else if (isItem(dataSourceClass)
                    && component instanceof Item.Viewer) {
                ((Item.Viewer) component).setItemDataSource((Item) method
                        .invoke(controller));
            }
        } catch (IllegalAccessException e) {
            throw new BinderException(e);
        } catch (InvocationTargetException e) {
            throw new BinderException(e);
        }
    }

    private Component tryToFindComponentById(Component root, String id) {
        Component component = Clara.findComponentById(root, id);
        if (component == null) {
            throw new BinderException("No component found for id: " + id + ".");
        }
        return component;
    }

    private boolean isContainer(Class<?> dataSourceClass) {
        return Container.class.isAssignableFrom(dataSourceClass);
    }

    private boolean isItem(Class<?> dataSourceClass) {
        return Item.class.isAssignableFrom(dataSourceClass);
    }

    private boolean isProperty(Class<?> dataSourceClass) {
        return Property.class.isAssignableFrom(dataSourceClass);
    }
}
TOP

Related Classes of org.vaadin.teemu.clara.binder.Binder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.