/**
*
*/
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.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 the ith component that matches the selector
* @param page the reference page for selector
* @param selector the selector string
* @param index 1-based index (1 means the first component found)
* @return Component, null if not found
*/
/*
public static Component find(Page page, String selector, int index) {
return getIthItem(new ComponentIterator(page, selector), index);
}
*/
/*
* Returns the ith component that matches the selector
* @param root root the reference component for selector
* @param selector selector the selector string
* @param index 1-based index (1 means the first component found)
* @return Component, null if not found
*/
/*
public static Component find(Component root, String selector, int index) {
return getIthItem(new ComponentIterator(root, selector), index);
}
*/
/**
* 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;
}
*/
}