/* 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);
}
}
}
}