zk/src/main/java/org/zkoss/zk/ui/select/Selectors.java

Summary

Maintainability
F
4 days
Test Coverage
/**
 * 
 */
package org.zkoss.zk.ui.select;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.slf4j.Logger;

import org.zkoss.lang.Strings;
import org.zkoss.xel.VariableResolver;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.select.annotation.Listen;
import org.zkoss.zk.ui.select.annotation.Wire;
import org.zkoss.zk.ui.select.annotation.WireVariable;
import org.zkoss.zk.ui.select.impl.ComponentIterator;
import org.zkoss.zk.ui.select.impl.Reflections;
import org.zkoss.zk.ui.select.impl.Reflections.FieldRunner;
import org.zkoss.zk.ui.select.impl.Reflections.MethodRunner;

/**
 * A collection of selector related utilities. 
 * @since 6.0.0
 * @author simonpai
 */
public class Selectors {

    /**
     * Returns an Iterable that iterates through all Components matched by the
     * selector. 
     * @param page the reference page for selector
     * @param selector the selector string
     * @return an Iterable of Component
     */
    public static Iterable<Component> iterable(final Page page, final String selector) {
        return new Iterable<Component>() {
            public Iterator<Component> iterator() {
                return new ComponentIterator(page, selector);
            }
        };
    }

    /**
     * Returns an Iterable that iterates through all Components matched by the
     * selector. 
     * @param root the reference component for selector
     * @param selector the selector string
     * @return an Iterable of Component
     */
    public static Iterable<Component> iterable(final Component root, final String selector) {
        return new Iterable<Component>() {
            public Iterator<Component> iterator() {
                return new ComponentIterator(root, selector);
            }
        };
    }

    /**
     * Returns a list of Components that match the selector.
     * @param page the reference page for selector
     * @param selector the selector string
     * @return a List of Component
     */
    public static List<Component> find(Page page, String selector) {
        return toList(iterable(page, selector));
    }

    /**
     * Returns a list of Components that match the selector.
     * @param root the reference component for selector
     * @param selector the selector string
     * @return a List of Component
     */
    public static List<Component> find(Component root, String selector) {
        return toList(iterable(root, selector));
    }

    /**
     * Returns a list of Components that match the selector (from the first page of current Desktop).
     * @param selector the selector string
     * @return a List of Component
     * @since 9.5.0
     */
    public static List<Component> find(String selector) {
        return toList(iterable(Executions.getCurrent().getDesktop().getFirstPage(), selector));
    }


    /**
     * Wire variables to controller, including XEL variables, implicit variables.
     * @param component the reference component
     * @param controller the controller object to be injected with variables
     */
    public static void wireVariables(Component component, Object controller, List<VariableResolver> extraResolvers) {
        new Wirer(controller, false).wireVariables(new ComponentFunctor(component), extraResolvers);
    }

    /**
     * Wire variables to controller, including XEL variables, implicit variables.
     * @param page the reference page
     * @param controller the controller object to be injected with variables
     */
    public static void wireVariables(Page page, Object controller, List<VariableResolver> extraResolvers) {
        new Wirer(controller, false).wireVariables(new PageFunctor(page), extraResolvers);
    }

    /**
     * Wire components to controller.
     * @param component the reference component for selector
     * @param controller the controller object to be injected with variables
     * @param ignoreNonNull ignore wiring when the value of the field is a 
     * Component (non-null) or a non-empty Collection.
     */
    public static void wireComponents(Component component, Object controller, boolean ignoreNonNull) {
        new Wirer(controller, false).wireComponents(new ComponentFunctor(component), ignoreNonNull);
    }

    /**
     * Wire components to controller.
     * @param page the reference page for selector
     * @param controller the controller object to be injected with variables
     * @param ignoreNonNull ignore wiring when the value of the field is a 
     * Component (non-null) or a non-empty Collection.
     */
    public static void wireComponents(Page page, Object controller, boolean ignoreNonNull) {
        new Wirer(controller, false).wireComponents(new PageFunctor(page), ignoreNonNull);
    }

    /**
     * Rewire the variables on session activation
     * @since 7.0.7
     */
    public static void rewireVariablesOnActivate(Component component, Object controller,
            List<VariableResolver> extraResolvers) {
        // called when activated
        new Wirer(controller, true).wireVariables(new ComponentFunctor(component), extraResolvers);
    }

    /**
     * Rewire the components on session activation
     * @since 7.0.7
     */
    public static void rewireComponentsOnActivate(Component component, Object controller) {
        // called when activated
        new Wirer(controller, true).wireComponents(new ComponentFunctor(component), false);
    }

    /**
     * Add event listeners to components based on the controller.
     * @param component the reference component for selector 
     * @param controller the controller of event listening methods
     */
    public static void wireEventListeners(final Component component, final Object controller) {
        wireEventListeners0(component, controller, false);
    }

    private static void wireEventListeners0(final Component component, final Object controller, final boolean rewire) {
        Reflections.forMethods(controller.getClass(), Listen.class, new MethodRunner<Listen>() {
            public void onMethod(Class<?> clazz, Method method, Listen anno) {
                // check method signature
                if ((method.getModifiers() & Modifier.STATIC) != 0)
                    throw new UiException("Cannot add forward to static method: " + method.getName());
                // method should have 0 or 1 parameter
                if (method.getParameterTypes().length > 1)
                    throw new UiException(
                            "Event handler method should have " + "at most one parameter: " + method.getName());
                for (String[] strs : splitListenAnnotationValues(anno.value())) {
                    String name = strs[0];
                    if (name == null)
                        name = "onClick";
                    // http://tracker.zkoss.org/browse/ZK-2582
                    int prio = 0;
                    int idx = name.indexOf('(');
                    if (idx > 0) {
                         int li = name.indexOf(')');
                        prio = Integer.parseInt(name.substring(idx + 1, li));
                        name = name.substring(0, idx);
                    }
                    Iterable<Component> iter = iterable(component, strs[1]);
                    // no forwarding, just add to event listener
                    Set<Component> rewired = rewire ? new HashSet<Component>() : null;
                    for (Component c : iter) {
                        if (rewired != null && !rewired.contains(c)) {
                            rewired.add(c);
                            c.removeAttribute(EVT_LIS);
                            Iterable<EventListener<? extends Event>> listeners = c.getEventListeners(name);
                            if (listeners != null) {
                                for (EventListener<? extends Event> listener : listeners)
                                    if (listener instanceof ComposerEventListener)
                                        c.removeEventListener(name, listener);
                            }
                        }
                        Set<String> set = getEvtLisSet(c, EVT_LIS);
                        String mhash = name + "#" + method.toString();
                        if (set.contains(mhash))
                            continue;
                        c.addEventListener(prio, name, new ComposerEventListener(method, controller));
                        set.add(mhash);
                    }
                }
            }
        });
    }

    /*package*/ static void rewireEventListeners(final Component component, final Object controller) {
        wireEventListeners0(component, controller, true);
    }

    private static final String EVT_LIS = "_SELECTOR_COMPOSER_EVENT_LISTENERS";

    @SuppressWarnings("unchecked")
    private static Set<String> getEvtLisSet(Component comp, String name) {
        Object obj = comp.getAttribute(name);
        if (obj != null)
            return (Set<String>) obj;
        Set<String> set = new HashSet<String>();
        comp.setAttribute(name, set);
        return set;
    }

    /** Creates a list of instances of {@link VariableResolver} based
     * on the annotation of the given class.
     * If no such annotation is found, an empty list is returned.
     * @param cls the class to look for the annotation.
     * @param untilClass the class to stop the searching.
     * By default, it will look for the annotation of the super class if not found.
     * Ignored if null.
     */
    public static List<VariableResolver> newVariableResolvers(Class<?> cls, Class<?> untilClass) {
        final List<VariableResolver> resolvers = new ArrayList<VariableResolver>();
        while (cls != null && cls != untilClass) {
            final org.zkoss.zk.ui.select.annotation.VariableResolver anno = cls
                    .getAnnotation(org.zkoss.zk.ui.select.annotation.VariableResolver.class);
            if (anno != null)
                for (Class<? extends VariableResolver> rc : anno.value()) {
                    try {
                        resolvers.add(rc.getConstructor().newInstance());
                    } catch (Exception e) {
                        throw UiException.Aide.wrap(e);
                    }
                }
            cls = cls.getSuperclass();
        }
        return resolvers;
    }

    // helper //
    private static String[][] splitListenAnnotationValues(String str) {
        List<String[]> result = new ArrayList<String[]>();
        int len = str.length();
        boolean inSqBracket = false;
        boolean inQuote = false;
        boolean escaped = false;
        String evtName = null;
        int i = 0;

        for (int j = 0; j < len; j++) {
            char c = str.charAt(j);

            if (!escaped)
                switch (c) {
                case '[':
                    inSqBracket = true;
                    break;
                case ']':
                    inSqBracket = false;
                    break;
                case '"':
                case '\'':
                    inQuote = !inQuote;
                    break;
                case '=':
                    if (inSqBracket || inQuote)
                        break;
                    if (evtName != null)
                        throw new UiException("Illegal value of @Listen: " + str);
                    evtName = str.substring(i, j).trim();
                    // check event name: onX
                    if (evtName.length() < 3 || !evtName.startsWith("on") || !Character.isUpperCase(evtName.charAt(2)))
                        throw new UiException("Illegal value of @Listen: " + str);
                    i = j + 1;
                    break;
                case ';':
                    if (inQuote)
                        break;
                    String target = str.substring(i, j).trim();
                    // check selector string: nonempty
                    if (target.length() == 0)
                        throw new UiException("Illegal value of @Listen: " + str);
                    result.add(new String[] { evtName, target });
                    i = j + 1;
                    evtName = null;
                    break;
                default:
                    // do nothing
                }

            escaped = !escaped && c == '\\';
        }

        // flush last chunk if any
        if (i < len) {
            String last = str.substring(i).trim();
            if (last.length() > 0)
                result.add(new String[] { evtName, last });
        }
        return result.toArray(new String[0][0]);
    }

    private static <T> List<T> toList(Iterable<T> iterable) {
        List<T> result = new ArrayList<T>();
        for (T t : iterable)
            result.add(t);
        return result;
    }

    // helper: auto wire //
    private static class Wirer {

        private final Object _controller;
        private final boolean _rewire;

        private Wirer(Object controller, final boolean rewire) {
            _controller = controller;
            _rewire = rewire;
        }

        private void wireComponents(final PsdoCompFunctor functor, final boolean ignoreNonNull) {
            final Class<?> ctrlClass = _controller.getClass();
            // wire to fields
            Reflections.forFields(ctrlClass, Wire.class, new FieldRunner<Wire>() {
                public void onField(Class<?> clazz, Field field, Wire anno) {
                    if ((field.getModifiers() & Modifier.STATIC) != 0)
                        throw new UiException("Cannot wire variable to " + "static field: " + field.getName());

                    if (_rewire && !anno.rewireOnActivate())
                        return; // skipped, not rewired

                    if (ignoreNonNull) {
                        // if not null && not collection, skip
                        Object value = Reflections.getFieldValue(_controller, field);
                        if (value != null && (!(value instanceof Collection<?>) || !((Collection<?>) value).isEmpty()))
                            return;
                    }

                    String selector = anno.value();
                    if (!Strings.isEmpty(selector))
                        injectComponent(field, functor.iterable(selector));

                    else {
                        // no selector value, wire implicit object by naming convention
                        Component value = getComponentByName(functor, field.getName(), field.getType());
                        if (value != null)
                            Reflections.setFieldValue(_controller, field, value);
                    }

                }
            });
            // wire by methods
            Reflections.forMethods(ctrlClass, Wire.class, new MethodRunner<Wire>() {
                public void onMethod(Class<?> clazz, Method method, Wire anno) {
                    // check method signature
                    String name = method.getName();
                    if ((method.getModifiers() & Modifier.STATIC) != 0)
                        throw new UiException("Cannot wire component by static method: " + name);
                    Class<?>[] paramTypes = method.getParameterTypes();
                    if (paramTypes.length != 1)
                        throw new UiException("Setter method should have only" + " one parameter: " + name);

                    if (_rewire && !anno.rewireOnActivate())
                        return; // skipped, not rewired

                    String selector = anno.value();
                    // check selector string: nonempty
                    if (!Strings.isEmpty(selector))
                        injectComponent(method, functor.iterable(selector));

                    else {
                        Component value = getComponentByName(functor, desetterize(method.getName()), paramTypes[0]);
                        Reflections.invokeMethod(method, this, value);
                    }
                }
            });
        }

        private void wireVariables(final PsdoCompFunctor functor, final List<VariableResolver> resolvers) {
            Class<?> ctrlClass = _controller.getClass();
            // wire to fields
            Reflections.forFields(ctrlClass, WireVariable.class, new FieldRunner<WireVariable>() {
                public void onField(Class<?> clazz, Field field, WireVariable anno) {
                    if ((field.getModifiers() & Modifier.STATIC) != 0)
                        throw new UiException("Cannot wire variable to " + "static field: " + field.getName());

                    if (_rewire && !anno.rewireOnActivate() && !isSessionOrWebApp(field.getType()))
                        return; // skipped, not rewired

                    String name = anno.value();
                    if (Strings.isEmpty(name))
                        name = guessImplicitObjectName(field.getType());

                    if (Strings.isEmpty(name))
                        name = field.getName();

                    Object value = getObjectByName(functor, name, field.getType(), resolvers);
                    if (value != null)
                        Reflections.setFieldValue(_controller, field, value);
                }
            });
            // wire by methods
            Reflections.forMethods(ctrlClass, WireVariable.class, new MethodRunner<WireVariable>() {
                public void onMethod(Class<?> clazz, Method method, WireVariable anno) {
                    // check method signature
                    String mname = method.getName();
                    if ((method.getModifiers() & Modifier.STATIC) != 0)
                        throw new UiException("Cannot wire variable by static" + " method: " + mname);
                    Class<?>[] paramTypes = method.getParameterTypes();
                    if (paramTypes.length != 1)
                        throw new UiException("Setter method should have" + " exactly one parameter: " + mname);

                    if (_rewire && !anno.rewireOnActivate() && !isSessionOrWebApp(paramTypes[0]))
                        return; // skipped, not rewired

                    String name = anno.value();
                    if (Strings.isEmpty(name))
                        name = guessImplicitObjectName(paramTypes[0]);

                    if (Strings.isEmpty(name))
                        name = desetterize(method.getName());

                    Object value = getObjectByName(functor, name, paramTypes[0], resolvers);
                    Reflections.invokeMethod(method, _controller, value);
                }
            });
        }

        private void injectComponent(Method method, Iterable<Component> iter) {
            injectComponent(new MethodFunctor(method), iter);
        }

        private void injectComponent(Field field, Iterable<Component> iter) {
            injectComponent(new FieldFunctor(field), iter);
        }

        @SuppressWarnings("unchecked")
        private void injectComponent(InjectionFunctor injector, Iterable<Component> comps) {
            Class<?> type = injector.getType();
            boolean isField = injector instanceof FieldFunctor;
            // Array
            if (type.isArray()) {
                injector.inject(_controller, generateArray(type.getComponentType(), comps));
                return;
            }
            // Collection
            if (Collection.class.isAssignableFrom(type)) {

                Collection collection = null;
                if (isField) {
                    Field field = ((FieldFunctor) injector).getField();
                    try {
                        collection = (Collection) field.get(_controller);
                    } catch (Exception e) {
                        throw new IllegalStateException(
                                "Field " + field + " not accessible or not declared by" + _controller);
                    }
                }

                // try to give an instance if null 
                if (collection == null) {
                    collection = getCollectionInstanceIfPossible(type);
                    if (collection == null)
                        throw new UiException("Cannot initiate collection for " + (isField ? "field" : "method") + ": "
                                + injector.getName() + " on " + _controller);
                    if (isField)
                        injector.inject(_controller, collection);
                }

                // try add to collection
                collection.clear();
                for (Component c : comps)
                    if (Reflections.isAppendableToCollection(injector.getGenericType(), c))
                        collection.add(c);
                if (!isField)
                    injector.inject(_controller, collection);
                return;
            }
            // set to field once or invoke method once
            for (Component c : comps) {
                if (!type.isInstance(c))
                    continue;
                injector.inject(_controller, c);
                return;
            }
            injector.inject(_controller, null); // no match, inject null
        }

        private Component getComponentByName(PsdoCompFunctor functor, String name, Class<?> type) {
            Component result = functor.getFellowIfAny(name);
            return isValidValue(result, type) ? result : null;
        }

        private Object getObjectByName(PsdoCompFunctor functor, String name, Class<?> type,
                List<VariableResolver> resolvers) {

            Object result = functor.getXelVariable(name);
            if (isValidValue(result, type))
                return result;

            if (resolvers != null)
                for (VariableResolver resv : resolvers) {
                    result = resv.resolveVariable(name);
                    if (isValidValue(result, type))
                        return result;
                }

            result = functor.getImplicit(name);
            return isValidValue(result, type) ? result : null;
        }

        private interface InjectionFunctor {
            public void inject(Object obj, Object value);

            public String getName();

            public Class<?> getType();

            public Type getGenericType();
        }

        private class FieldFunctor implements InjectionFunctor {
            private final Field _field;

            private FieldFunctor(Field field) {
                _field = field;
            }

            public void inject(Object obj, Object value) {
                Reflections.setFieldValue(obj, _field, value);
            }

            public String getName() {
                return _field.getName();
            }

            public Class<?> getType() {
                return _field.getType();
            }

            public Type getGenericType() {
                return _field.getGenericType();
            }

            public Field getField() {
                return _field;
            }
        }

        private class MethodFunctor implements InjectionFunctor {
            private final Method _method;

            private MethodFunctor(Method method) {
                _method = method;
            }

            public void inject(Object obj, Object value) {
                Reflections.invokeMethod(_method, obj, value);
            }

            public String getName() {
                return _method.getName();
            }

            public Class<?> getType() {
                return _method.getParameterTypes()[0];
            }

            public Type getGenericType() {
                return _method.getGenericParameterTypes()[0];
            }
        }

    }

    private static String guessImplicitObjectName(Class<?> cls) {
        if (Execution.class.equals(cls))
            return "execution";
        if (Page.class.equals(cls))
            return "page";
        if (Desktop.class.equals(cls))
            return "desktop";
        if (Session.class.equals(cls))
            return "session";
        if (WebApp.class.equals(cls))
            return "application";
        if (Logger.class.equals(cls))
            return "log";
        return null;
    }

    private static boolean isSessionOrWebApp(Class<?> cls) {
        return Session.class.equals(cls) || WebApp.class.equals(cls);
    }

    private static boolean isValidValue(Object value, Class<?> clazz) {
        return value != null && clazz.isAssignableFrom(value.getClass());
    }

    private static String desetterize(String name) {
        if (name.length() < 4 || !name.startsWith("set") || Character.isLowerCase(name.charAt(3)))
            throw new UiException("Expecting method name in form setXxx: " + name);
        return Character.toLowerCase(name.charAt(3)) + name.substring(4);
    }

    @SuppressWarnings("unchecked")
    private static Collection getCollectionInstanceIfPossible(Class<?> clazz) {
        if (clazz.isAssignableFrom(ArrayList.class))
            return new ArrayList();
        if (clazz.isAssignableFrom(HashSet.class))
            return new HashSet();
        if (clazz.isAssignableFrom(TreeSet.class))
            return new TreeSet();
        try {
            return (Collection) clazz.getConstructor().newInstance();
        } catch (Exception e) {
            // ignore
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private static <T> T[] generateArray(Class<T> clazz, Iterable<Component> comps) {
        // add to a temporary ArrayList then set to Array
        ArrayList<T> list = new ArrayList<T>();
        for (Component c : comps)
            if (clazz.isAssignableFrom(c.getClass()))
                list.add((T) c);
        return list.toArray((T[]) Array.newInstance(clazz, 0));
    }

    // Cannot be serialized
    public static class ComposerEventListener implements EventListener<Event> {

        private final Method _ctrlMethod;
        private final Object _ctrl;

        public ComposerEventListener(Method method, Object controller) {
            _ctrlMethod = method;
            _ctrl = controller;
        }

        public void onEvent(Event event) throws Exception {
            if (_ctrlMethod.getParameterTypes().length == 0)
                _ctrlMethod.invoke(_ctrl);
            else
                _ctrlMethod.invoke(_ctrl, event);
        }
    }

    // helper: functor //
    private interface PsdoCompFunctor {
        public Iterable<Component> iterable(String selector);

        public Object getImplicit(String name);

        public Object getAttribute(String name);

        public Object getXelVariable(String name);

        public Component getFellowIfAny(String name);
    }

    private static class PageFunctor implements PsdoCompFunctor {
        private final Page _page;

        private PageFunctor(Page page) {
            _page = page;
        }

        public Iterable<Component> iterable(String selector) {
            return Selectors.iterable(_page, selector);
        }

        public Object getImplicit(String name) {
            return Components.getImplicit(_page, name);
        }

        public Object getXelVariable(String name) {
            return _page.getXelVariable(null, null, name, true);
        }

        public Object getAttribute(String name) {
            return _page.getAttribute(name, true);
        }

        public Component getFellowIfAny(String name) {
            return _page.getFellowIfAny(name);
        }
    }

    private static class ComponentFunctor implements PsdoCompFunctor {
        private final Component _comp;

        private ComponentFunctor(Component comp) {
            _comp = comp;
        }

        public Iterable<Component> iterable(String selector) {
            return Selectors.iterable(_comp, selector);
        }

        public Object getImplicit(String name) {
            return Components.getImplicit(_comp, name);
        }

        public Object getXelVariable(String name) {
            return getPage().getXelVariable(null, null, name, true);
        }

        public Object getAttribute(String name) {
            return _comp.getAttribute(name, true);
        }

        public Component getFellowIfAny(String name) {
            return _comp.getFellowIfAny(name);
        }

        private Page getPage() {
            return Components.getCurrentPage(_comp);
        }
    }

    /*
    private static <T> T getIthItem(Iterator<T> iter, int index){
        // shift (index - 1) times
        for(int i = 1; i < index; i++) {
            if(!iter.hasNext())
                return null;
            iter.next();
        }
        return iter.hasNext() ? iter.next() : null;
    }
    */

}