org.zkoss.bind.BindComposer Maven / Gradle / Ivy
/* BindComposer.java
Purpose:
Description:
History:
Jun 22, 2011 10:09:50 AM, Created by henrichen
Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/
package org.zkoss.bind;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.bind.annotation.AfterCompose;
import org.zkoss.bind.annotation.HistoryPopState;
import org.zkoss.bind.annotation.ToServerCommand;
import org.zkoss.bind.impl.AbstractAnnotatedMethodInvoker;
import org.zkoss.bind.impl.AnnotationUtil;
import org.zkoss.bind.impl.BindEvaluatorXUtil;
import org.zkoss.bind.impl.BinderImpl;
import org.zkoss.bind.impl.MiscUtil;
import org.zkoss.bind.impl.ValidationMessagesImpl;
import org.zkoss.bind.init.ViewModelAnnotationResolvers;
import org.zkoss.bind.sys.BindEvaluatorX;
import org.zkoss.bind.sys.BinderCtrl;
import org.zkoss.bind.sys.ValidationMessages;
import org.zkoss.bind.sys.debugger.BindingAnnotationInfoChecker;
import org.zkoss.bind.sys.debugger.DebuggerFactory;
import org.zkoss.bind.tracker.impl.BindUiLifeCycle;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Strings;
import org.zkoss.util.CacheMap;
import org.zkoss.util.EmptyCacheMap;
import org.zkoss.util.IllegalSyntaxException;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuService;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.HistoryPopStateEvent;
import org.zkoss.zk.ui.event.SerializableEventListener;
import org.zkoss.zk.ui.event.UploadEvent;
import org.zkoss.zk.ui.metainfo.Annotation;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.util.Callback;
import org.zkoss.zk.ui.util.ComponentActivationListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.ComposerExt;
import org.zkoss.zk.ui.util.ConventionWires;
/**
* Base composer to apply ZK Bind.
*
* @author henrichen
* @since 6.0.0
*/
@SuppressWarnings("rawtypes")
public class BindComposer
implements Composer, ComposerExt, Serializable, AuService, ComponentActivationListener {
private static final long serialVersionUID = 1463169907348730644L;
private static final Logger _log = LoggerFactory.getLogger(BindComposer.class);
public static final String VM_ID = "$VM_ID$";
public static final String BINDER_ID = "$BINDER_ID$";
public static final String ON_BIND_COMMAND = "onBindCommand$";
public static final String ON_BIND_GLOBAL_COMMAND = "onBindGlobalCommand$";
public static final String ON_BIND_COMMAND_UPLOAD = "onBindCommandUpload$";
private Object _viewModel;
private AnnotateBinder _binder;
private final Map _converters;
private final Map _validators;
private final BindEvaluatorX evalx;
protected static final String ID_ANNO = "id";
protected static final String INIT_ANNO = "init";
protected static final String VALUE_ANNO_ATTR = "value";
protected static final String VIEW_MODEL_ATTR = "viewModel";
protected static final String BINDER_ATTR = "binder";
protected static final String VALIDATION_MESSAGES_ATTR = "validationMessages";
protected static final String QUEUE_NAME_ANNO_ATTR = "queueName";
protected static final String QUEUE_SCOPE_ANNO_ATTR = "queueScope";
private static final Map, List> _afterComposeMethodCache = BinderImpl.DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, List>(
600, CacheMap.DEFAULT_LIFETIME);
private static final Map, List> _historyPopStateMethodCache = BinderImpl.DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, List>(
600, CacheMap.DEFAULT_LIFETIME);
public BindComposer() {
setViewModel(this);
_converters = new HashMap(8);
_validators = new HashMap(8);
evalx = BindEvaluatorXUtil.createEvaluator(null);
}
public Binder getBinder() {
return _binder;
}
//can assign a separate view model, default to this
public void setViewModel(Object viewModel) {
_viewModel = viewModel;
if (this._binder != null) {
//do view model proxy
Object vm = this._binder.createViewModelProxyIfEnabled(_viewModel);
this._binder.setViewModel(vm);
_viewModel = vm;
}
}
public Object getViewModel() {
return _viewModel;
}
public Converter getConverter(String name) {
Converter conv = _converters.get(name);
return conv;
}
public Validator getValidator(String name) {
Validator validator = _validators.get(name);
return validator;
}
public void addConverter(String name, Converter converter) {
_converters.put(name, converter);
}
public void addValidator(String name, Validator validator) {
_validators.put(name, validator);
}
//--ComposerExt//
public ComponentInfo doBeforeCompose(Page page, Component parent, ComponentInfo compInfo) throws Exception {
return compInfo;
}
public void doBeforeComposeChildren(final Component comp) throws Exception {
//ZK-3831
if (comp.getPage() == null) {
final Map, ?> currentArg = Executions.getCurrent().getArg();
((ComponentCtrl) comp).addCallback(ComponentCtrl.AFTER_PAGE_ATTACHED, new Callback() {
public void call(Object data) {
try {
Executions.getCurrent().pushArg(currentArg);
doBeforeComposeChildren(comp);
} catch (Exception e) {
throw UiException.Aide.wrap(e);
} finally {
Executions.getCurrent().popArg();
}
}
});
return;
}
//init viewmodel first
_viewModel = initViewModel(evalx, comp);
_binder = initBinder(evalx, comp);
//do view model proxy
if (!this.equals(_viewModel)) {
Object vmProxy = _binder.createViewModelProxyIfEnabled(_viewModel);
if (vmProxy != null) {
_viewModel = vmProxy;
comp.setAttribute((String) comp.getAttribute(VM_ID), vmProxy);
}
}
ValidationMessages vmsgs = initValidationMessages(evalx, comp, _binder);
//wire before call init
Selectors.wireVariables(comp, _viewModel, Selectors.newVariableResolvers(BindUtils.getViewModelClass(_viewModel), null));
if (vmsgs != null) {
_binder.setValidationMessages(vmsgs);
}
try {
BinderKeeper keeper = BinderKeeper.getInstance(comp);
keeper.book(_binder, comp);
_binder.init(comp, _viewModel, getViewModelInitArgs(evalx, comp));
} catch (Exception x) {
throw MiscUtil.mergeExceptionInfo(x, comp);
}
//to apply composer-name
ConventionWires.wireController(comp, this);
}
//--Composer--//
public void doAfterCompose(final T comp) throws Exception {
//ZK-3831
if (comp.getPage() == null) {
final Map, ?> currentArg = Executions.getCurrent().getArg();
((ComponentCtrl) comp).addCallback(ComponentCtrl.AFTER_PAGE_ATTACHED, new Callback() {
public void call(Object data) {
try {
Executions.getCurrent().pushArg(currentArg);
doAfterCompose(comp);
} catch (Exception e) {
throw UiException.Aide.wrap(e);
} finally {
Executions.getCurrent().popArg();
}
}
});
return;
}
_binder.initAnnotatedBindings();
// trigger ViewModel's @AfterCompose method.
new AbstractAnnotatedMethodInvoker(AfterCompose.class, _afterComposeMethodCache) {
protected boolean shouldLookupSuperclass(AfterCompose annotation) {
return annotation.superclass();
}
}.invokeMethod(_binder, getViewModelInitArgs(evalx, comp));
// call loadComponent
BinderKeeper keeper = BinderKeeper.getInstance(comp);
if (keeper.isRootBinder(_binder)) {
keeper.loadComponentForAllBinders();
}
comp.setAuService(this);
// ZK-3711 Listen to HistoryPopStateEvent if @HistoryPopState exists.
final AbstractAnnotatedMethodInvoker historyPopStateInvoker =
new AbstractAnnotatedMethodInvoker(HistoryPopState.class, _historyPopStateMethodCache) {
protected boolean shouldLookupSuperclass(HistoryPopState annotation) {
return false;
}
};
if (historyPopStateInvoker.hasAnnotatedMethod(_binder)) {
Page page = comp.getPage();
if (page != null) {
page.addEventListener(Events.ON_HISTORY_POP_STATE, new SerializableEventListener() {
// ZK-4061: Prevent from duplicated handling because of multiple root components
private HistoryPopStateEvent _handling = null;
public void onEvent(HistoryPopStateEvent event) throws Exception {
if (event != _handling) {
_handling = event;
historyPopStateInvoker.invokeMethod(getBinder(), null, event, true);
}
}
});
}
}
}
private Map getViewModelInitArgs(BindEvaluatorX evalx, Component comp) {
final ComponentCtrl compCtrl = (ComponentCtrl) comp;
final Collection anncol = compCtrl.getAnnotations(VIEW_MODEL_ATTR, INIT_ANNO);
if (anncol.size() == 0)
return null;
final Annotation ann = anncol.iterator().next();
final Map attrs = ann.getAttributes(); //(tag, tagExpr)
Map args = null;
for (final Iterator> it = attrs.entrySet().iterator(); it.hasNext();) {
final Entry entry = it.next();
final String tag = entry.getKey();
final String[] tagExpr = entry.getValue();
if ("value".equals(tag)) {
//ignore
} else { //other unknown tag, keep as arguments
if (args == null) {
args = new HashMap();
}
args.put(tag, tagExpr);
}
}
return args == null ? null : BindEvaluatorXUtil.parseArgs(_binder.getEvaluatorX(), args);
}
private Object initViewModel(BindEvaluatorX evalx, Component comp) {
final ComponentCtrl compCtrl = (ComponentCtrl) comp;
final Annotation idanno = compCtrl.getAnnotation(VIEW_MODEL_ATTR, ID_ANNO);
final Annotation initanno = compCtrl.getAnnotation(VIEW_MODEL_ATTR, INIT_ANNO);
String vmname = null;
Object vm = null;
BindingAnnotationInfoChecker checker = getBindingAnnotationInfoChecker();
if (checker != null) {
checker.checkViewModel(comp);
}
if (idanno == null && initanno == null) {
return _viewModel;
} else if (idanno == null) {
throw new IllegalSyntaxException(
MiscUtil.formatLocationMessage("you have to use @id to assign the name of view model", comp));
} else if (initanno == null) {
throw new IllegalSyntaxException(
MiscUtil.formatLocationMessage("you have to use @init to assign the view model", comp));
}
vmname = BindEvaluatorXUtil.eval(evalx, comp,
AnnotationUtil.testString(idanno.getAttributeValues(VALUE_ANNO_ATTR), idanno), String.class);
vm = BindEvaluatorXUtil.eval(evalx, comp,
AnnotationUtil.testString(initanno.getAttributeValues(VALUE_ANNO_ATTR), initanno), Object.class);
if (Strings.isEmpty(vmname)) {
throw new UiException(MiscUtil.formatLocationMessage("name of view model is empty", idanno));
}
try {
if (vm instanceof String) {
Page page = comp.getPage();
if (page == null) {
throw new UiException(MiscUtil.formatLocationMessage(
"can't find Page to resolve a view model class :'" + vm + "'", initanno));
} else {
vm = comp.getPage().resolveClass((String) vm);
}
}
if (vm instanceof Class>) {
vm = ((Class>) vm).newInstance();
}
} catch (Exception e) {
throw MiscUtil.mergeExceptionInfo(e, initanno);
}
if (vm == null) {
throw new UiException(MiscUtil.formatLocationMessage("view model of '" + vmname + "' is null", initanno));
} else if (vm.getClass().isPrimitive()) {
throw new UiException(MiscUtil
.formatLocationMessage("view model '" + vmname + "' is a primitive type, is " + vm, initanno));
}
comp.setAttribute(vmname, vm);
comp.setAttribute(VM_ID, vmname);
return vm;
}
private AnnotateBinder initBinder(BindEvaluatorX evalx, Component comp) {
final ComponentCtrl compCtrl = (ComponentCtrl) comp;
final Annotation idanno = compCtrl.getAnnotation(BINDER_ATTR, ID_ANNO);
final Annotation initanno = compCtrl.getAnnotation(BINDER_ATTR, INIT_ANNO);
Object binder = null;
String bname = null;
BindingAnnotationInfoChecker checker = getBindingAnnotationInfoChecker();
if (checker != null) {
checker.checkBinder(comp);
}
if (idanno != null) {
bname = BindEvaluatorXUtil.eval(evalx, comp,
AnnotationUtil.testString(idanno.getAttributeValues(VALUE_ANNO_ATTR), idanno), String.class);
if (Strings.isEmpty(bname)) {
throw new UiException(MiscUtil.formatLocationMessage("name of binder is empty", idanno));
}
} else {
bname = BINDER_ATTR;
}
if (initanno != null) {
binder = AnnotationUtil.testString(initanno.getAttributeValues(VALUE_ANNO_ATTR), initanno);
String name = AnnotationUtil.testString(initanno.getAttributeValues(QUEUE_NAME_ANNO_ATTR), initanno);
String scope = AnnotationUtil.testString(initanno.getAttributeValues(QUEUE_SCOPE_ANNO_ATTR), initanno);
//if no binder, create default binder with custom queue name and scope
String expr;
if (name != null) {
name = BindEvaluatorXUtil.eval(evalx, comp, expr = name, String.class);
if (Strings.isBlank(name)) {
throw new UiException(MiscUtil
.formatLocationMessage("evaluated queue name is empty, expression is " + expr, initanno));
}
}
if (scope != null) {
scope = BindEvaluatorXUtil.eval(evalx, comp, expr = scope, String.class);
if (Strings.isBlank(scope)) {
throw new UiException(MiscUtil
.formatLocationMessage("evaluated queue scope is empty, expression is " + expr, initanno));
}
}
if (binder != null) {
binder = BindEvaluatorXUtil.eval(evalx, comp, (String) binder, Object.class);
try {
if (binder instanceof String) {
binder = comp.getPage().resolveClass((String) binder);
}
if (binder instanceof Class>) {
binder = ((Class>) binder).getDeclaredConstructor(String.class, String.class)
.newInstance(name, scope);
}
} catch (Exception e) {
throw UiException.Aide.wrap(e, e.getMessage());
}
if (!(binder instanceof AnnotateBinder)) {
throw new UiException(
MiscUtil.formatLocationMessage("evaluated binder is not a binder is " + binder, initanno));
}
} else {
binder = newAnnotateBinder(name, scope); //ZK-2288
}
} else {
binder = newAnnotateBinder(null, null); //ZK-2288
}
//put to attribute, so binder could be referred by the name
comp.setAttribute(bname, binder);
comp.setAttribute(BINDER_ID, bname);
return (AnnotateBinder) binder;
}
//ZK-2288: A way to specify a customized default AnnotateBinder.
private AnnotateBinder newAnnotateBinder(String name, String scope) {
String clznm = Library.getProperty("org.zkoss.bind.AnnotateBinder.class");
if (clznm != null) {
try {
return (AnnotateBinder) Classes.newInstanceByThread(clznm, new Class[] { String.class, String.class },
new String[] { name, scope });
} catch (Exception e) {
throw UiException.Aide.wrap(e, "Can't initialize binder");
}
} else {
return new AnnotateBinder(name, scope);
}
}
private ValidationMessages initValidationMessages(BindEvaluatorX evalx, Component comp, Binder binder) {
final ComponentCtrl compCtrl = (ComponentCtrl) comp;
final Annotation idanno = compCtrl.getAnnotation(VALIDATION_MESSAGES_ATTR, ID_ANNO);
final Annotation initanno = compCtrl.getAnnotation(VALIDATION_MESSAGES_ATTR, INIT_ANNO);
Object vmessages = null;
String vname = null;
BindingAnnotationInfoChecker checker = getBindingAnnotationInfoChecker();
if (checker != null) {
checker.checkValidationMessages(comp);
}
if (idanno != null) {
vname = BindEvaluatorXUtil.eval(evalx, comp,
AnnotationUtil.testString(idanno.getAttributeValues(VALUE_ANNO_ATTR), idanno), String.class);
if (Strings.isEmpty(vname)) {
throw new UiException(MiscUtil.formatLocationMessage("name of ValidationMessages is empty", idanno));
}
} else {
return null; //validation messages is default null
}
if (initanno != null) {
vmessages = BindEvaluatorXUtil.eval(evalx, comp,
AnnotationUtil.testString(initanno.getAttributeValues(VALUE_ANNO_ATTR), initanno), Object.class);
try {
if (vmessages instanceof String) {
vmessages = comp.getPage().resolveClass((String) vmessages);
}
if (vmessages instanceof Class>) {
vmessages = ((Class>) vmessages).newInstance();
}
} catch (Exception e) {
throw UiException.Aide.wrap(e, MiscUtil.formatLocationMessage(e.getMessage(), initanno));
}
if (!(vmessages instanceof ValidationMessages)) {
throw new UiException(MiscUtil.formatLocationMessage(
"evaluated validationMessages is not a ValidationMessages is " + vmessages, initanno));
}
} else {
vmessages = new ValidationMessagesImpl();
}
//put to attribute, so binder could be referred by the name
comp.setAttribute(vname, vmessages);
return (ValidationMessages) vmessages;
}
public boolean doCatch(Throwable ex) throws Exception {
return false;
}
public void doFinally() throws Exception {
// ignore
}
//--notifyChange--//
public void notifyChange(Object bean, String property) {
getBinder().notifyChange(bean, property);
}
// Bug fixed for B70-ZK-2843
public void didActivate(Component comp) {
Selectors.rewireVariablesOnActivate(comp, this.getViewModel(),
Selectors.newVariableResolvers(BindUtils.getViewModelClass(_viewModel), null));
}
public void willPassivate(Component comp) {
}
/**
* A parsing scope context for storing Binders, and handle there loadComponent
* invocation properly.
*
*
if component trees with bindings are totally separated( none of
* each contains another), then for each separated tree, there's only one keeper.
*
* @author Ian Y.T Tsai(zanyking)
*/
private static class BinderKeeper {
private static final String KEY_BINDER_KEEPER = "$BinderKeeper$";
/**
* get a Binder Keeper or create it by demand.
*
* @param comp
* @return
*/
static BinderKeeper getInstance(Component comp) {
BinderKeeper keeper = (BinderKeeper) comp.getAttribute(KEY_BINDER_KEEPER, true);
if (keeper == null) {
comp.setAttribute(KEY_BINDER_KEEPER, keeper = new BinderKeeper(comp));
}
return keeper;
}
private final LinkedList _queue;
private Component _host;
public BinderKeeper(final Component comp) {
_host = comp;
_queue = new LinkedList();
// ensure the keeper will always cleaned up
Events.postEvent("onRootBinderHostDone", comp, null);
comp.addEventListener("onRootBinderHostDone", new EventListener() {
public void onEvent(Event event) throws Exception {
//suicide first...
_host.removeEventListener("onRootBinderHostDone", this);
BinderKeeper keeper = (BinderKeeper) _host.getAttribute(KEY_BINDER_KEEPER);
if (keeper == null) {
// suppose to be null...
} else {
// The App is in trouble.
// some error might happened during page processing
// which cause loadComponent() never invoked.
_host.removeAttribute(KEY_BINDER_KEEPER);
}
}
});
}
public void book(Binder binder, Component comp) {
_queue.add(new Loader(binder, comp));
}
public boolean isRootBinder(Binder binder) {
return _queue.getFirst().binder == binder;
}
public void loadComponentForAllBinders() {
_host.removeAttribute(KEY_BINDER_KEEPER);
for (Loader loader : _queue) {
loader.load();
}
}
/**
* for Binder to load Component.
*
* @author Ian Y.T Tsai(zanyking)
*/
private static class Loader {
Binder binder;
Component comp;
public Loader(Binder binder, Component comp) {
super();
this.binder = binder;
this.comp = comp;
}
public void load() {
//ZK-1699, mark the comp and it's children are handling, to prevent load twice in include.src case
BindUiLifeCycle.markLifeCycleHandling(comp);
//load data
binder.loadComponent(comp, true); //load all bindings
}
} //end of class...
} //end of class...
private BindingAnnotationInfoChecker getBindingAnnotationInfoChecker() {
DebuggerFactory factory = DebuggerFactory.getInstance();
return factory == null ? null : factory.getAnnotationInfoChecker();
}
public boolean service(AuRequest request, boolean everError) {
final String cmd = request.getCommand();
if (cmd.startsWith(ON_BIND_COMMAND) || cmd.startsWith(ON_BIND_GLOBAL_COMMAND) || cmd.startsWith(ON_BIND_COMMAND_UPLOAD)) {
final Map data = request.getData();
String vcmd = data.get("cmd").toString();
final ToServerCommand ccmd = ViewModelAnnotationResolvers.getAnnotation(BindUtils.getViewModelClass(_viewModel), ToServerCommand.class);
List asList = new ArrayList();
if (ccmd != null) {
asList.addAll(Arrays.asList(ccmd.value()));
}
//ZK-3133
Map mmv = _binder.getMatchMediaValue();
if (!mmv.isEmpty()) {
asList.addAll(mmv.keySet());
}
if (asList != null) {
if (asList.contains("*") || asList.contains(vcmd)) {
if (cmd.startsWith(ON_BIND_COMMAND)) {
_binder.postCommand(vcmd, (Map) data.get("args"));
} else if (cmd.startsWith(ON_BIND_GLOBAL_COMMAND)) {
BindUtils.postGlobalCommand(_binder.getQueueName(), _binder.getQueueScope(), vcmd,
(Map) data.get("args"));
} else if (cmd.startsWith(ON_BIND_COMMAND_UPLOAD)) { // ZK-4472
_binder.postCommand(vcmd, Collections.singletonMap(BinderCtrl.CLIENT_UPLOAD_INFO, UploadEvent.getUploadEvent(vcmd, request.getComponent(), request)));
}
}
}
return true;
}
return false;
}
} //end of class...