/* ConventionWires.java Purpose: Description: History: Thu Dec 8 13:04:09 TST 2011, Created by tomyeh Copyright (C) 2011 Potix Corporation. All Rights Reserved. */ package org.zkoss.zk.ui.util; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zkoss.lang.Classes; import org.zkoss.lang.Library; import org.zkoss.util.Converter; import org.zkoss.util.Pair; import org.zkoss.zk.au.AuRequest; import org.zkoss.zk.au.AuService; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.IdSpace; import org.zkoss.zk.ui.Page; import org.zkoss.zk.ui.UiException; import org.zkoss.zk.ui.annotation.Command; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.select.impl.Reflections; /** * Utilities to wire variables by name conventions. * For wiring variables with annotations or with CSS3 selector, please use * {@link org.zkoss.zk.ui.select.Selectors} instead. * * @author tomyeh * @since 6.0.0 */ public class ConventionWires { private static final Logger log = LoggerFactory.getLogger(ConventionWires.class); /** Wire fellow components and space owner ancestors of the specified * Id space into a controller Java object. This implementation checks the * setXxx() method names first then the * field names. If a setXxx() method name matches the id of a fellow or * space owner ancestors and with correct * argument type, the found method is called with the fellow component as the * argument. If no proper setXxx() method then search the field of the * controller object for a matched field with name equals to the fellow * component's id and proper type. Then the fellow component * is assigned as the value of the matched field. * * <p>Note that fellow components are looked up first, then the space owner * ancestors<p> * <p>since 3.5.2, the controller would be assigned as a variable of the given idspace * per the naming convention composed of the idspace id and controller Class name. e.g. * if the idspace id is "xwin" and the controller class is * org.zkoss.MyController, then the variable name would be "xwin$MyController"</p> * * <p>This is useful in writing controller code in MVC design practice. You * can wire the components into the controller object per the * component's id and do whatever you like.</p> * * <p>Since 3.6.0, for Groovy or other environment that * '$' is not applicable, you can invoke {@link #wireFellows(IdSpace,Object,char)} * to use '_' as the separator. * * @param idspace the id space to be bound * @param controller the controller Java object to be injected the fellow components. */ public static final void wireFellows(IdSpace idspace, Object controller) { new ConventionWire(controller).wireFellows(idspace); } /** Wire fellow components and space owner with a custom separator. * The separator is used to separate the component ID and additional * information, such as event name. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke this method to use '_' as * the separator. * @see #wireFellows(IdSpace, Object) */ public static final void wireFellows(IdSpace idspace, Object controller, char separator) { new ConventionWire(controller, separator).wireFellows(idspace); } /** Wire fellow components and space owner with full control. * @param separator the separator used to separate the component ID and event name. * @param ignoreZScript whether to ignore variables defined in zscript when wiring * a member. * @param ignoreXel whether to ignore variables defined in varible resolver * ({@link Page#addVariableResolver}) when wiring a member. */ public static final void wireFellows(IdSpace idspace, Object controller, char separator, boolean ignoreZScript, boolean ignoreXel) { new ConventionWire(controller, separator, ignoreZScript, ignoreXel).wireFellows(idspace); } /** <p>Wire accessible variable objects of the specified component into a * controller Java object. This implementation checks the * setXxx() method names first then the field names. If a setXxx() method * name matches the name of the resolved variable object with correct * argument type and the associated field value is null, then the method is * called with the resolved variable object as the argument. * If no proper setXxx() method then search the * field name of the controller object. If the field name matches the name * of the resolved variable object with correct field type and null field * value, the field is then assigned the resolved variable object. * </p> * * <p>The controller would be assigned as a variable of the given component * per the naming convention composed of the component id and controller Class name. e.g. * if the component id is "xwin" and the controller class is * org.zkoss.MyController, then the variable name would be "xwin$MyController"</p> * * <p>This is useful in writing controller code in MVC design practice. You * can wire the embedded objects, components, and accessible variables into * the controller object per the components' id and variables' name and do * whatever you like. * </p> * <p>Since 3.6.0, for Groovy or other environment that * '$' is not applicable, you can invoke {@link #wireVariables(Component,Object,char)} * to use '_' as the separator. * * @param comp the reference component to wire variables * @param controller the controller Java object to be injected the * accessible variable objects. * @see org.zkoss.zk.ui.util.GenericAutowireComposer */ public static final void wireVariables(Component comp, Object controller) { new ConventionWire(controller).wireVariables(comp); } /** Wire accessible variable objects of the specified component with a custom separator. * The separator is used to separate the component ID and additional * information, such as event name. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke this method to use '_' as * the separator. * @see #wireVariables(Component, Object) */ public static final void wireVariables(Component comp, Object controller, char separator) { new ConventionWire(controller, separator).wireVariables(comp); } /** Wire controller as a variable objects of the specified component with full control. * @param separator the separator used to separate the component ID and event name. * @param ignoreZScript whether to ignore variables defined in zscript when wiring * a member. * @param ignoreXel whether to ignore variables defined in varible resolver * ({@link Page#addVariableResolver}) when wiring a member. */ public static final void wireVariables(Component comp, Object controller, char separator, boolean ignoreZScript, boolean ignoreXel) { new ConventionWire(controller, separator, ignoreZScript, ignoreXel).wireVariables(comp); } /** <p>Wire accessible variables of the specified page into a * controller Java object. This implementation checks the * setXxx() method names first then the field names. If a setXxx() method * name matches the name of the resolved variable object with correct * argument type and the associated field value is null, then the method is * called with the resolved variable object as the argument. * If no proper setXxx() method then search the * field name of the controller object. If the field name matches the name * of the resolved variable object with correct field type and null field * value, the field is then assigned the resolved variable object.</p> * * <p>The controller would be assigned as a variable of the given page * per the naming convention composed of the page id and controller Class name. e.g. * if the page id is "xpage" and the controller class is * org.zkoss.MyController, then the variable name would be "xpage$MyController"</p> * * <p>Since 3.0.8, if the method name of field name matches the ZK implicit * object name, ZK implicit object will be wired in, too.</p> * <p>This is useful in writing controller code in MVC design practice. You * can wire the embedded objects, components, and accessible variables into * the controller object per the component's id and variable name and do * whatever you like. * </p> * * <p>Since 3.6.0, for Groovy or other environment that * '$' is not applicable, you can invoke {@link #wireVariables(Page,Object,char)} * to use '_' as the separator. * * @param page the reference page to wire variables * @param controller the controller Java object to be injected the fellow components. */ public static final void wireVariables(Page page, Object controller) { new ConventionWire(controller).wireVariables(page); } /** Wire accessible variable objects of the specified page with a custom separator. * The separator is used to separate the component ID and additional * information, such as event name. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke this method to use '_' as * the separator. * @see #wireVariables(Page, Object) */ public static final void wireVariables(Page page, Object controller, char separator) { new ConventionWire(controller, separator).wireVariables(page); } /** Wire accessible variable objects of the specified page with complete control. * @param separator the separator used to separate the component ID and event name. * @param ignoreZScript whether to ignore variables defined in zscript when wiring * a member. * @param ignoreXel whether to ignore variables defined in variable resolver * ({@link Page#addVariableResolver}) when wiring a member. */ public static final void wireVariables(Page page, Object controller, char separator, boolean ignoreZScript, boolean ignoreXel) { new ConventionWire(controller, separator, ignoreZScript, ignoreXel).wireVariables(page); } /** Wire controller as an attribute of the specified component. * Please refer to <a href="http://books.zkoss.org/wiki/ZK_Developer%27s_Reference/MVC/Controller/Composer">ZK Developer's Reference</a> * for details. * <p>The separator is used to separate the component ID and the controller. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke {@link #wireController(Component, Object, char)} * to use '_' as the separator. */ public static final void wireController(Component comp, Object controller) { wireController(comp, controller, '$'); } /** Wire controller as an attribute of the specified component with a custom separator. * <p>The separator is used to separate the component ID and the controller. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke this method to use '_' as * the separator. */ public static final void wireController(Component comp, Object controller, char separator) { //feature #3326788: support custom name Object onm = comp.getAttribute("composerName"); if (onm instanceof String && ((String) onm).length() > 0) { comp.setAttribute((String) onm, controller); } else { //bug zk-1298, the timing doesn't correct to get composerName in doBeforeComposeChildren //fix by post processing in AttributesInfo#apply comp.setAttribute("_$composer$_", controller); //stored in a special attribute } //after the fix of zk-1298, the id-$composer is always available no matter the composerName exsited or not. comp.setAttribute(separator + "composer", controller); //no need to check since it is more nature (new overwrites old) //feature #2778513, support {id}$composer name final String id = comp.getId(); comp.setAttribute(id + separator + "composer", controller); //support {id}$ClassName comp.setAttribute(composerNameByClass(id, controller.getClass(), separator), controller); } private interface WireAuService extends AuService { } /** * Wire controller's command method to be an AuService command that the command * can be triggered from client side JavaScript. * <p>For example,</p> * <pre><code> * zkservice.$('$foo').command('commandName', [{bar: 0}, {bar2: 'bar2'}]); * </code></pre> * <p>Developer can specify the <tt>org.zkoss.zk.ui.jsonServiceParamConverter.class</tt> * in zk.xml or lang-addon.xml to provide a specific JSON converter which implements {@link Converter} * </p> * @param comp * @param controller * @since 8.0.0 */ public static final void wireServiceCommand(final Component comp, final Object controller) { Reflections.forMethods(controller.getClass(), Command.class, new Reflections.MethodRunner<Command>() { public void onMethod(Class<?> clazz, final Method method, Command annotation) { if ((method.getModifiers() & Modifier.STATIC) != 0) throw new UiException("Cannot add forward to static method: " + method.getName()); AuService auService = comp.getAuService(); // no need to chain the same WireAuService it happens in a serializable case final AuService prevAuService = auService instanceof WireAuService ? null : auService; comp.setAuService(new WireAuService() { private Converter<Pair<Class<?>, Object>, Object> converter; { String property = Library.getProperty("org.zkoss.zk.ui.jsonServiceParamConverter.class"); if (property == null) { converter = new Converter<Pair<Class<?>, Object>, Object>() { public Object convert(Pair<Class<?>, Object> pair) { return pair.getY(); } }; } else { try { converter = (Converter) Classes.newInstanceByThread(property); } catch (Exception x) { log.error(x.getMessage(), x); } } } public boolean service(AuRequest request, boolean everError) { String command = request.getCommand(); if (command.startsWith("onAuServiceCommand$")) { Map<String, Object> data = request.getData(); final String cmd = (String) data.get("cmd"); final List<Object> args = (List<Object>) data.get("args"); final List<String> stringList = new LinkedList<String>( Arrays.asList(method.getAnnotation(Command.class).value())); stringList.add(method.getName()); if (stringList.contains(cmd)) { try { Class<?>[] types = method.getParameterTypes(); if (types.length == 0) method.invoke(controller); else { if (args == null || args.size() != types.length) { throw new IllegalArgumentException( "The number of the parameters from the json value are not the same as the method parameters"); } Object[] params = new Object[types.length]; int index = 0; for (Class<?> type : types) { params[index] = converter .convert(new Pair<Class<?>, Object>(type, args.get(index))); index++; } method.invoke(controller, params); } } catch (Exception e) { throw UiException.Aide.wrap(e); } } return true; } if (prevAuService != null) return prevAuService.service(request, everError); return false; } }); } }); } private static String composerNameByClass(String id, Class cls, char separator) { final String clsname = cls.getName(); int j = clsname.lastIndexOf('.'); return id + separator + (j >= 0 ? clsname.substring(j + 1) : clsname); } /**Wire implicit variables of the specified component into a controller Java object. * * @param comp the component * @param controller the controller object */ public static final void wireImplicit(Component comp, Object controller) { new ConventionWire(controller, '$', true, true).wireImplicit(comp); } /** <p>Adds forward conditions to myid source component so onXxx source * event received by * myid component can be forwarded to the specified target * component with the target event name onXxx$myid.</p> * <p>The controller is a POJO file with onXxx$myid methods (the event handler * codes). This utility method search such onXxx$myid methods and adds * forward condition to the source myid component looked up by * {@link Component#getAttributeOrFellow} of the specified component, so you * don't have to specify in zul file the "forward" attribute one by one. * If the source component cannot be looked up or the object looked up is * not a component, this method will log the error and ignore it. * </p> * <p>Cascaded '$' will add Forwards cascadedly. E.g. define method * onClick$btn$w1 in window w2. This method will add a forward on the button * "btn.onClick=w1.onClick$btn" and add another forward on the window w1 * "w1.onClick$btn=w2.onClick$btn$w1"</p> * * <p>Since 3.6.0, for Groovy or other environment that * '$' is not applicable, you can invoke {@link #addForwards(Component,Object,char)} * to use '_' as the separator. * * @param comp the targetComponent * @param controller the controller code with onXxx$myid event handler methods */ public static void addForwards(Component comp, Object controller) { addForwards(comp, controller, '$'); } /** Adds forward conditions to the specified component with a custom separator. * The separator is used to separate the component ID and additional * information, such as event name. * By default, it is '$'. However, for Groovy or other environment that * '$' is not applicable, you can invoke this method to use '_' as * the separator. * @see #addForwards(Component, Object) */ public static void addForwards(Component comp, Object controller, char separator) { final Class cls = controller.getClass(); final Method[] mtds = cls.getMethods(); for (int j = 0; j < mtds.length; ++j) { final Method md = mtds[j]; String mdname = md.getName(); if (mdname.length() >= 5 && Events.isValid(mdname)) { //onX$Y Component xcomp = comp; int k = 0; do { //handle cascade $. e.g. onClick$btn$win1 k = mdname.lastIndexOf(separator); if (k >= 3) { //found '$' final String srcevt = mdname.substring(0, k); if ((k + 1) < mdname.length()) { final String srccompid = mdname.substring(k + 1); Object srccomp = xcomp.getAttributeOrFellow(srccompid, true); if (srccomp == null) { Page page = xcomp.getPage(); if (page != null) srccomp = page.getXelVariable(null, null, srccompid, true); } if (srccomp == null || !(srccomp instanceof Component)) { if (log.isDebugEnabled()) log.debug("Cannot find the associated component to forward event: " + mdname); break; } else { ((Component) srccomp).addForward(srcevt, xcomp, mdname); xcomp = (Component) srccomp; mdname = srcevt; } } else { throw new UiException( "Illegal event method name(component id not specified or consecutive '" + separator + "'): " + md.getName()); } } } while (k >= 3); } } } }