All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.zkoss.bind.impl.BinderImpl Maven / Gradle / Ivy

There is a newer version: 10.0.0-jakarta
Show newest version
/* BinderImpl.java

	Purpose:
		
	Description:
		
	History:
		Jul 29, 2011 6:08:51 PM, Created by henrichen

Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/

package org.zkoss.bind.impl;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.bind.BindComposer;
import org.zkoss.bind.BindContext;
import org.zkoss.bind.BindUtils;
import org.zkoss.bind.Binder;
import org.zkoss.bind.Converter;
import org.zkoss.bind.Form;
import org.zkoss.bind.FormLegacy;
import org.zkoss.bind.FormLegacyExt;
import org.zkoss.bind.GlobalCommandEvent;
import org.zkoss.bind.Phase;
import org.zkoss.bind.PhaseListener;
import org.zkoss.bind.Property;
import org.zkoss.bind.PropertyChangeEvent;
import org.zkoss.bind.SimpleForm;
import org.zkoss.bind.Validator;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.DefaultCommand;
import org.zkoss.bind.annotation.DefaultGlobalCommand;
import org.zkoss.bind.annotation.Destroy;
import org.zkoss.bind.annotation.GlobalCommand;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.MatchMedia;
import org.zkoss.bind.annotation.NotifyCommand;
import org.zkoss.bind.annotation.NotifyCommands;
import org.zkoss.bind.annotation.SmartNotifyChange;
import org.zkoss.bind.init.ViewModelAnnotationResolvers;
import org.zkoss.bind.init.ZKBinderPhaseListeners;
import org.zkoss.bind.proxy.FormProxyObject;
import org.zkoss.bind.proxy.ViewModelProxyObject;
import org.zkoss.bind.sys.BindEvaluatorX;
import org.zkoss.bind.sys.BinderCtrl;
import org.zkoss.bind.sys.Binding;
import org.zkoss.bind.sys.CommandBinding;
import org.zkoss.bind.sys.ConditionType;
import org.zkoss.bind.sys.FormBinding;
import org.zkoss.bind.sys.InitChildrenBinding;
import org.zkoss.bind.sys.InitFormBinding;
import org.zkoss.bind.sys.InitPropertyBinding;
import org.zkoss.bind.sys.LoadBinding;
import org.zkoss.bind.sys.LoadChildrenBinding;
import org.zkoss.bind.sys.LoadFormBinding;
import org.zkoss.bind.sys.LoadPropertyBinding;
import org.zkoss.bind.sys.PropertyBinding;
import org.zkoss.bind.sys.ReferenceBinding;
import org.zkoss.bind.sys.SaveBinding;
import org.zkoss.bind.sys.SaveFormBinding;
import org.zkoss.bind.sys.SavePropertyBinding;
import org.zkoss.bind.sys.TemplateResolver;
import org.zkoss.bind.sys.ValidationMessages;
import org.zkoss.bind.sys.debugger.BindingAnnotationInfoChecker;
import org.zkoss.bind.sys.debugger.BindingExecutionInfoCollector;
import org.zkoss.bind.sys.debugger.DebuggerFactory;
import org.zkoss.bind.sys.debugger.impl.DefaultAnnotationInfoChecker;
import org.zkoss.bind.sys.debugger.impl.DefaultExecutionInfoCollector;
import org.zkoss.bind.sys.debugger.impl.info.AddBindingInfo;
import org.zkoss.bind.sys.debugger.impl.info.AddCommandBindingInfo;
import org.zkoss.bind.sys.debugger.impl.info.CommandInfo;
import org.zkoss.bind.sys.debugger.impl.info.EventInfo;
import org.zkoss.bind.sys.debugger.impl.info.NotifyChangeInfo;
import org.zkoss.bind.sys.tracker.Tracker;
import org.zkoss.bind.tracker.impl.TrackerImpl;
import org.zkoss.bind.xel.zel.BindELContext;
import org.zkoss.bind.xel.zel.ImplicitObjectELResolver;
import org.zkoss.json.JSONObject;
import org.zkoss.json.JSONValue;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.util.CacheMap;
import org.zkoss.util.EmptyCacheMap;
import org.zkoss.util.Pair;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.AbstractComponent;
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.HtmlShadowElement;
import org.zkoss.zk.ui.ShadowElement;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.ClientInfoEvent;
import org.zkoss.zk.ui.event.Deferrable;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.EventQueue;
import org.zkoss.zk.ui.event.EventQueues;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.metainfo.Annotation;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.ComponentActivationListener;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.ExecutionInit;

/**
 * Implementation of Binder.
 * @author henrichen
 * @author dennischen
 * @since 6.0.0
 */
public class BinderImpl implements Binder, BinderCtrl, Serializable {
	private static final long serialVersionUID = 1463169907348730644L;

	private static final Logger _log = LoggerFactory.getLogger(BinderImpl.class);

	private static final Map RENDERERS = new HashMap();

	//events for dummy target
	private static final String ON_POST_COMMAND = "onPostCommand";
	private static final String ON_VMSGS_CHANGED = "onVMsgsChanged";

	//ZK-4761: for debug mode only
	public static final boolean DISABLE_METHOD_CACHE;
	private static final String DISABLE_METHOD_CACHE_PROP = "org.zkoss.bind.disableMethodCache";

	static {
		DISABLE_METHOD_CACHE = Boolean.parseBoolean(Library.getProperty(DISABLE_METHOD_CACHE_PROP, "false"));
	}

	private static final Map, List> _initMethodCache = DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, List>(600,
			CacheMap.DEFAULT_LIFETIME); //class,list
	private static final Map, List> _destroyMethodCache = DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, List>(600,
			CacheMap.DEFAULT_LIFETIME); //class,list

	private static final Map, Map>> _commandMethodCache = DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, Map>>(
			200, CacheMap.DEFAULT_LIFETIME); //class,map

	private static final Map, Map>> _globalCommandMethodCache = DISABLE_METHOD_CACHE ? new EmptyCacheMap() : new CacheMap, Map>>(
			200, CacheMap.DEFAULT_LIFETIME); //class,map

	//command and default command method parsing and caching 
	private static final CachedItem NULL_METHOD = new CachedItem(null);
	private static final String COMMAND_METHOD_MAP_INIT = "$INIT_FLAG$";
	private static final String COMMAND_METHOD_DEFAULT = "$DEFAULT_FLAG$";
	private static final CommandMethodInfoProvider _commandMethodInfoProvider = new CommandMethodInfoProvider() {
		public String getAnnotationName() {
			return Command.class.getSimpleName();
		}

		public String getDefaultAnnotationName() {
			return DefaultCommand.class.getSimpleName();
		}

		public String[] getCommandName(Method method) {
			final Command cmd = ViewModelAnnotationResolvers.getAnnotation(method, Command.class);
			return cmd == null ? null : cmd.value();
		}

		public boolean isDefaultMethod(Method method) {
			return ViewModelAnnotationResolvers.getAnnotation(method, DefaultCommand.class) != null;
		}
	};
	private static final CommandMethodInfoProvider _globalCommandMethodInfoProvider = new CommandMethodInfoProvider() {
		public String getAnnotationName() {
			return GlobalCommand.class.getSimpleName();
		}

		public String getDefaultAnnotationName() {
			return DefaultGlobalCommand.class.getSimpleName();
		}

		public String[] getCommandName(Method method) {
			final GlobalCommand cmd = ViewModelAnnotationResolvers.getAnnotation(method, GlobalCommand.class);
			return cmd == null ? null : cmd.value();
		}

		public boolean isDefaultMethod(Method method) {
			return ViewModelAnnotationResolvers.getAnnotation(method, DefaultGlobalCommand.class) != null;
		}
	};

	private Component _rootComp;
	private BindEvaluatorX _eval;
	private transient List _phaseListeners;
	private Tracker _tracker;
	private final Component _dummyTarget = new AbstractComponent(); //a dummy target for post command

	/* holds all binding in this binder */
	private final Map>> _bindings; //comp -> (evtnm | _fieldExpr | formid) -> bindings

	private final FormBindingHandler _formBindingHandler;
	private final PropertyBindingHandler _propertyBindingHandler;
	private final ChildrenBindingHandler _childrenBindingHandler;
	private final ReferenceBindingHandler _refBindingHandler;

	/* the relation of form and inner save-bindings */
	private Map> _assocFormSaveBindings; //form comp -> savebindings	
	private Map>> _reversedAssocFormSaveBindings; //associated comp -> binding -> associated save bindings of _formSaveBindingMap

	private final Map _listenerMap; //comp+evtnm -> eventlistener
	private final String _quename;
	private final String _quescope;
	private final QueueListener _queueListener;

	private ValidationMessages _validationMessages;
	private Set _hasValidators; //the key to mark they have validator

	private final Map> _templateResolvers; //comp,

	//flag to keep info of current vm has converter method or not
	private boolean _hasGetConverterMethod = true;

	//flag to keep info of current vm has validator method or not
	private boolean _hasGetValidatorMethod = true;

	//flag to keep info of the binding is initialized or not.
	private boolean _init = false;

	//flag to keep info that binder is in activating state
	private boolean _activating = false;
	//to help deferred activation when first execution
	private transient DeferredActivator _deferredActivator;

	private transient Map> _saveFormFields;

	private final ImplicitObjectContributor _implicitContributor;

	private static final String REF_HANDLER_CLASS_PROP = "org.zkoss.bind.ReferenceBindingHandler.class";

	//ZK-3133 to cache MatchMedia annotation values
	private transient Map _matchMediaValues;

	//ZK-4791
	private static final Pattern CALL_OTHER_VM_COMMAND_PATTERN = Pattern.compile("\\$([^.]*)\\..*$");

	// ZK-4855, internal used only
	public static final String ZKFORMPROXYNOTIFIEDKEY = "$$zkFormProxyNotified$$";

	public BinderImpl() {
		this(null, null);
	}

	public BinderImpl(String qname, String qscope) {
		_bindings = new HashMap>>();
		_formBindingHandler = new FormBindingHandler();
		_propertyBindingHandler = new PropertyBindingHandler();
		_childrenBindingHandler = new ChildrenBindingHandler();

		_formBindingHandler.setBinder(this);
		_propertyBindingHandler.setBinder(this);
		_childrenBindingHandler.setBinder(this);

		_refBindingHandler = MiscUtil.newInstanceFromProperty(REF_HANDLER_CLASS_PROP, null,
				ReferenceBindingHandler.class);
		if (_refBindingHandler != null) {
			_refBindingHandler.setBinder(this);
		}

		//zk-1548
		_implicitContributor = new ImplicitObjectContributorImpl();

		_assocFormSaveBindings = new HashMap>();
		_reversedAssocFormSaveBindings = new HashMap>>();

		_hasValidators = new HashSet();
		_templateResolvers = new HashMap>();
		_listenerMap = new HashMap();
		//use same queue name if user was not specified, 
		//this means, binder in same scope, same queue, they will share the notification by "base"."property" 
		_quename = qname != null && !Strings.isEmpty(qname) ? qname : DEFAULT_QUEUE_NAME;
		_quescope = qscope != null && !Strings.isBlank(qscope) ? qscope : DEFAULT_QUEUE_SCOPE;
		_queueListener = new QueueListener();
		init();
	}

	private void init() {
		_phaseListeners = new LinkedList(ZKBinderPhaseListeners.getSystemPhaseListeners());

		String clz = Library.getProperty(PHASE_LISTENER_CLASS_KEY);
		if (!Strings.isEmpty(clz)) {
			try {
				addPhaseListener((PhaseListener) Classes.forNameByThread(clz).newInstance());
			} catch (Exception e) {
				_log.error("Error when initial phase listener:" + clz, e);
			}
		}
	}
	
	/**
	 * @since 6.0.1
	 */
	public void init(Component comp, Object viewModel, Map initArgs) {
		if (_init)
			throw new UiException("binder is already initialized");
		_init = true;
		
		_rootComp = comp;
		//initial associated view model
		setViewModel(viewModel);
		_dummyTarget.addEventListener(ON_POST_COMMAND, new PostCommandListener());
		_dummyTarget.addEventListener(ON_VMSGS_CHANGED, new VMsgsChangedListener());
		
		initQueue();

		if (viewModel instanceof Composer && !(viewModel instanceof BindComposer)) { //do we need to warn this?
			//show a warn only
			_log.warn("you are using a composer [{}] as a view model", viewModel);
		}
		new AbstractAnnotatedMethodInvoker(Init.class, _initMethodCache) {
			protected boolean shouldLookupSuperclass(Init annotation) {
				return annotation.superclass();
			}
		}.invokeMethod(this, initArgs);

		initActivator();
		//ZK-3133
		_matchMediaValues = initMatchMediaValues(viewModel);
		if (!_matchMediaValues.isEmpty()) {
			Clients.response(new AuInvoke(_rootComp, "$binder"));
			final Execution exec = Executions.getCurrent();
			if (exec != null) {
				Cookie[] cookies = ((HttpServletRequest) Executions.getCurrent().getNativeRequest()).getCookies();
				String[] matchMedias = null;
				JSONObject args = new JSONObject();
				if (cookies != null) {
					for (Cookie c : cookies) {
						// ZKMatchMeida and ZKClientInfo are both refer to the cookie names in Binder.js
						String name = c.getName();
						String value = c.getValue();
						try {
							value = URLDecoder.decode(value, "UTF-8");
						} catch (UnsupportedEncodingException e) {
							_log.error("Failed to decode cookie " + name, e);
							continue;
						}
						if ("ZKMatchMedia".equals(name)) {
							matchMedias = value.trim().split(",");
						} else if ("ZKClientInfo".equals(name)) {
							args.put(BinderCtrl.CLIENT_INFO, JSONValue.parse(value));
						}
						if (matchMedias != null && args.size() != 0) {
							if (!matchMedias[0].isEmpty()) {
								for (String s : matchMedias) {
									if (!_matchMediaValues.containsKey(s))
										continue;
									final Event evt = new Event(ON_POST_COMMAND, _dummyTarget, new Object[] { s, args });
									Events.postEvent(-1, evt);
								}
							}
							break;
						}
					}
				}
			}
		}
	}

	private Map initMatchMediaValues(Object viewModel) {
		Map values = new HashMap<>(6);
		for (Method m : BindUtils.getViewModelClass(viewModel).getMethods()) {
			MatchMedia annomm = ViewModelAnnotationResolvers.getAnnotation(m, MatchMedia.class);
			if (annomm != null) {
				for (String s : annomm.value()) {
					s = BinderCtrl.MATCHMEDIAVALUE_PREFIX + s.trim();
					if (values.containsKey(s))
						throw new UiException("there are more then one MatchMedia method \"" + s.substring(16)
								+ "\" in class " + viewModel);
					values.put(s, m);
				}
			}
		}
		return values.isEmpty() ? Collections.emptyMap() : values;
	}

	public void destroy(Component comp, Object viewModel) {
		new AbstractAnnotatedMethodInvoker(Destroy.class, _destroyMethodCache) {
			protected boolean shouldLookupSuperclass(Destroy annotation) {
				return annotation.superclass();
			}
		}.invokeMethod(this, null);
	}

	private class QueueListener implements EventListener, Serializable {
		private static final long serialVersionUID = 1L;

		public void onEvent(Event event) throws Exception {
			//only when a event in queue is our event
			if (event instanceof PropertyChangeEvent) {
				final PropertyChangeEvent evt = (PropertyChangeEvent) event;
				BinderImpl.this.doPropertyChange(evt.getBase(), evt.getProperty());
			} else if (event instanceof GlobalCommandEvent) {
				final GlobalCommandEvent evt = (GlobalCommandEvent) event;
				final Set notifys = new LinkedHashSet();
				BinderImpl.this.doGlobalCommand(_rootComp, evt.getCommand(), evt.getTriggerEvent(), evt.getArgs(), notifys);
				fireNotifyChanges(notifys);
				notifyVMsgsChanged();
			}
		}
	}

	protected void checkInit() {
		if (!_init) {
			throw new UiException("binder is not initialized yet");
		}
	}

	public Map> getBindings(Component comp) {
		return _bindings.get(comp);
	}

	//called when onPropertyChange is fired to the subscribed event queue
	private void doPropertyChange(Object base, String prop) {
		String debugInfo = MessageFormat.format("doPropertyChange: base=[{0}],prop=[{1}]", base, prop);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}

		//zk-1468, 
		//ignore a coming ref-binding if the binder is the same since it was loaded already.
		if (base instanceof ReferenceBinding && ((ReferenceBinding) base).getBinder() == this) {
			return;
		}

		//ZK-4855
		if (base instanceof FormProxyObject) {
			Execution execution = Executions.getCurrent();
			Set> zkProxyNotified = execution != null ? (Set>) execution.getAttribute(ZKFORMPROXYNOTIFIEDKEY) : null;
			if (zkProxyNotified != null)
				zkProxyNotified.remove(new Pair<>(base, prop)); //try to remove flag
		}

		final Tracker tracker = getTracker();
		final Set bindings = tracker.getLoadBindings(base, prop);
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		try {
			if (collector != null) {
				collector.pushStack("NOTIFY_CHANGE");
				collector.addInfo(new NotifyChangeInfo(_rootComp, base, prop, "Size=" + bindings.size()));
			}
			doPropertyChange0(base, prop, bindings);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			if (collector != null) {
				collector.popStack();
			}
		}
	}

	private void doPropertyChange0(Object base, String prop, Set bindings) {
		Execution exec = Executions.getCurrent();
		Set skipCheckChildren = (Set) exec.getAttribute(HtmlShadowElement.SKIP_DISTRIBUTED_CHILDREN_PROPERTY_CHANGE);
		for (LoadBinding binding : bindings) {
			//BUG 828, the sub-sequence binding might be removed after the previous loading.
			final Component comp = binding.getComponent();
			if (!(comp instanceof ShadowElement) && (comp == null || comp.getPage() == null))
				continue;

			boolean skip = false;
			if (skipCheckChildren != null)
				for (Component skipComp : skipCheckChildren)
					if (Components.isAncestor(skipComp, comp)) {
						skip = true;
						break;
					}
			if (skip)
				continue;

			final BindContext ctx = BindContextUtil.newBindContext(this, binding, false, null, comp, null);
			if (binding instanceof PropertyBinding) {
				BindContextUtil.setConverterArgs(this, comp, ctx, (PropertyBinding) binding);
			}

			String debugInfo = MessageFormat.format("doPropertyChange:binding.load() "
					+ "binding=[{0}],context=[{1}]", binding, ctx);
			try {
				if (_log.isDebugEnabled()) {
					_log.debug(debugInfo);
				}
				doPrePhase(Phase.LOAD_BINDING, ctx);
				binding.load(ctx);
			} catch (Exception ex) {
				throw new RuntimeException(debugInfo, ex);
			} finally {
				doPostPhase(Phase.LOAD_BINDING, ctx);
			}

			//zk-1468, 
			//notify the ref-binding changed since other nested binder might use it
			if (binding instanceof ReferenceBinding && binding != base) {
				notifyChange(binding, ".");
			}

			if (_validationMessages != null) {
				String attr = null;
				if (binding instanceof PropertyBinding) {
					attr = ((PropertyBinding) binding).getFieldName();
				} else if (binding instanceof FormBinding) {
					attr = ((FormBinding) binding).getFormId();
				} else {
					//ignore children binding
				}
				if (attr != null && hasValidator(comp, attr)) {
					_validationMessages.clearMessages(comp, attr);
				}
			}
		}
	}

	public void setViewModel(Object vm) {
		checkInit();
		_rootComp.setAttribute(BinderCtrl.VM, vm);
		_hasGetConverterMethod = true; //reset to true
		_hasGetValidatorMethod = true; //reset to true
		collectNotifyCommands(vm);
	}

	private transient Map _notifyCommands;

	private void collectNotifyCommands(Object vm) {
		Class viewModelClz = BindUtils.getViewModelClass(vm);
		NotifyCommands commands = ViewModelAnnotationResolvers.getAnnotation(viewModelClz, NotifyCommands.class);
		NotifyCommand command = ViewModelAnnotationResolvers.getAnnotation(viewModelClz, NotifyCommand.class);
		if (_notifyCommands != null)
			_notifyCommands.clear();

		if (command != null) {
			for (String cmd : command.value()) {
				_notifyCommands = AllocUtil.inst.putMap(_notifyCommands, cmd, command);
			}
		}
		if (commands != null) {
			for (NotifyCommand nc : commands.value()) {
				for (String cmd : nc.value()) {
					_notifyCommands = AllocUtil.inst.putMap(_notifyCommands, cmd, nc);
				}
			}
		}
	}

	public Object getViewModel() {
		checkInit();
		return getOriginViewModel(_rootComp.getAttribute(BinderCtrl.VM));
	}

	private Object getViewModelInView() {
		checkInit();
		return _rootComp.getAttribute(BinderCtrl.VM);
	}

	private static Object getOriginViewModel(Object vm) {
		if (vm instanceof ViewModelProxyObject)
			vm = ((ViewModelProxyObject) vm).getOriginObject();
		return vm;
	}

	//Note: assume system converter is state-less
	public Converter getConverter(String name) {
		checkInit();
		Converter converter = null;
		if (_hasGetConverterMethod) {
			Object vm = getViewModel();
			Class clz = BindUtils.getViewModelClass(vm);
			Method m = null;
			Object result = null;
			try {
				m = clz.getMethod("getConverter", String.class);
			} catch (SecurityException x) {
				_hasGetConverterMethod = false;
			} catch (NoSuchMethodException e) {
				_hasGetConverterMethod = false;
			}
			if (m != null) {
				try {
					result = m.invoke(vm, name);
				} catch (IllegalArgumentException e) {
					throw new RuntimeException(e.getMessage(), e);
				} catch (IllegalAccessException e) {
					_hasGetConverterMethod = false;
				} catch (InvocationTargetException e) {
					throw new RuntimeException(e.getMessage(), e);
				}
				if (result != null && !(result instanceof Converter)) {
					_hasGetConverterMethod = false;
				} else {
					converter = (Converter) result;
				}
			}
		}
		if (converter == null) {
			converter = SystemConverters.get(name);
		}
		if (converter == null) {
			throw new UiException("Cannot find converter:" + name);
		}
		return converter;
	}

	//Note: assume system validator is state-less
	public Validator getValidator(String name) {
		checkInit();
		Validator validator = null;
		if (_hasGetValidatorMethod) {
			Object vm = getViewModel();
			Class clz = BindUtils.getViewModelClass(vm);
			Method m = null;
			Object result = null;
			try {
				m = clz.getMethod("getValidator", String.class);
			} catch (SecurityException x) {
				_hasGetValidatorMethod = false;
			} catch (NoSuchMethodException e) {
				_hasGetValidatorMethod = false;
			}
			if (m != null) {
				try {
					result = m.invoke(vm, name);
				} catch (IllegalArgumentException e) {
					throw new RuntimeException(e.getMessage(), e);
				} catch (IllegalAccessException e) {
					_hasGetValidatorMethod = false;
				} catch (InvocationTargetException e) {
					throw new RuntimeException(e.getMessage(), e);
				}
				if (result != null && !(result instanceof Validator)) {
					_hasGetValidatorMethod = false;
				} else {
					validator = (Validator) result;
				}
			}
		}
		if (validator == null) {
			validator = SystemValidators.get(name);
		}
		if (validator == null) {
			throw new UiException("Cannot find validator:" + name);
		}
		return validator;
	}

	//Note: assume system renderer is state-less 
	protected Object getRenderer(String name) {
		Object renderer = RENDERERS.get(name);
		if (renderer == null && name.indexOf('.') > 0) { //might be a class path
			try {
				renderer = Classes.newInstanceByThread(name);
				RENDERERS.put(name, renderer); //assume renderer is state-less
			} catch (IllegalAccessException e) {
				throw UiException.Aide.wrap(e, e.getMessage());
			} catch (Exception e) {
				//ignore
			}
		}
		return renderer;
	}

	public BindEvaluatorX getEvaluatorX() {
		if (_eval == null) {
			_eval = BindEvaluatorXUtil.createEvaluator(null);
		}
		return _eval;
	}

	public void storeForm(Component comp, String id, Form form) {
		final String oldid = (String) comp.getAttribute(FORM_ID, Component.COMPONENT_SCOPE);
		//check if a form exist already, allow to store a form with same id again for replacing the form 
		if (oldid != null && !oldid.equals(id)) {
			throw new IllegalArgumentException(
					"try to store 2 forms in same component id : 1st " + oldid + ", 2nd " + id);
		}
		final Form oldForm = (Form) comp.getAttribute(id);

		if (oldForm == form)
			return;

		comp.setAttribute(FORM_ID, id); //mark it is a form component with the form id;
		comp.setAttribute(id, form); //after setAttribute, we can access fx in el.

		if (form instanceof FormLegacyExt) {
			final FormLegacyExt fex = (FormLegacyExt) form;
			comp.setAttribute(id + "Status", fex.getStatus()); //by convention fxStatus
			
			if (oldForm instanceof FormLegacyExt) { //copy the filed information, this is for a form-init that assign a user form
				for (String fn : ((FormLegacyExt) oldForm).getLoadFieldNames()) {
					fex.addLoadFieldName(fn);
				}
				for (String fn : ((FormLegacyExt) oldForm).getSaveFieldNames()) {
					fex.addSaveFieldName(fn);
				}
			}
			return;
		}
		if (form instanceof Form) {
			final Form fex = form;
			comp.setAttribute(id + "Status", fex.getFormStatus()); //by convention fxStatus
		}
		Map> initSaveFormMap = initSaveFormMap();
		Set remove = initSaveFormMap.remove(oldForm);
		if (remove != null) {
			Set set = initSaveFormMap.get(form);
			if (set == null) {
				set = new HashSet(16);
				initSaveFormMap.put(form, set);
			}
			set.addAll(remove);
		}
	}

	public Form getForm(Component comp, String id) {
		String oldid = (String) comp.getAttribute(FORM_ID, Component.COMPONENT_SCOPE);
		if (oldid == null || !oldid.equals(id)) {
			//return null if the id is not correct
			return null;
		}
		return (Form) comp.getAttribute(id);
	}

	private void removeForm(Component comp) {
		String id = (String) comp.getAttribute(FORM_ID, Component.COMPONENT_SCOPE);
		if (id != null) {
			comp.removeAttribute(FORM_ID);
			Object form = comp.removeAttribute(id);
			if (form != null)
				initSaveFormMap().remove(form);
			comp.removeAttribute(id + "Status");
		}
	}

	private void initFormLegacyBean(Component comp, String id, Object bean) {
		Form form = getForm(comp, id);
		if (form == null && bean instanceof FormLegacy)
			storeForm(comp, id, new SimpleForm());
	}

	public void addFormInitBinding(Component comp, String id, String initExpr, Map initArgs) {
		checkInit();
		if (Strings.isBlank(id)) {
			throw new IllegalArgumentException(MiscUtil.formatLocationMessage("form id is blank", comp));
		}
		if (initExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("initExpr is null for component " + comp + ", form " + id, comp));
		}

		addFormInitBinding0(comp, id, initExpr, initArgs);

	}

	private void addFormInitBinding0(Component comp, String formId, String initExpr, Map bindingArgs) {

		if (_log.isDebugEnabled()) {
			_log.debug("add init-form-binding: comp=[{}],form=[{}],expr=[{}]", comp, formId, initExpr);
		}
		final String attr = formId;

		InitFormBinding binding = newInitFormBinding(comp, attr, initExpr, bindingArgs);

		addBinding(comp, attr, binding);
		final BindingKey bkey = getBindingKey(comp, attr);
		_formBindingHandler.addInitBinding(bkey, binding);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_INIT, comp, null, binding.getPropertyString(),
					formId, bindingArgs, null));
		}
		BindContext ctx = BindContextUtil.newBindContext(this, binding, false, null, comp, null);
		final Object bean = getEvaluatorX().getValue(ctx, comp, ((InitFormBindingImpl) binding)._accessInfo.getProperty());
		initFormLegacyBean(comp, formId, bean);
	}

	public void addFormLoadBindings(Component comp, String id, String loadExpr, String[] beforeCmds, String[] afterCmds,
			Map bindingArgs) {
		checkInit();
		if (Strings.isBlank(id)) {
			throw new IllegalArgumentException(MiscUtil.formatLocationMessage("form id is blank", comp));
		}
		if (loadExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("loadExpr is null for component " + comp + ", form " + id, comp));
		}

		addFormLoadBindings0(comp, id, loadExpr, beforeCmds, afterCmds, bindingArgs);
	}

	public void addFormSaveBindings(Component comp, String id, String saveExpr, String[] beforeCmds, String[] afterCmds,
			Map bindingArgs, String validatorExpr, Map validatorArgs) {
		checkInit();
		if (Strings.isBlank(id)) {
			throw new IllegalArgumentException(MiscUtil.formatLocationMessage("form id is blank", comp));
		}
		if (saveExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("saveExpr is null for component " + comp + ", form " + id, comp));
		}

		addFormSaveBindings0(comp, id, saveExpr, beforeCmds, afterCmds, bindingArgs, validatorExpr, validatorArgs);
	}

	private void addFormLoadBindings0(Component comp, String formId, String loadExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs) {
		final boolean prompt = isPrompt(beforeCmds, afterCmds);
		final String attr = formId;
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (prompt) {
			final LoadFormBinding binding = newLoadFormBinding(comp, formId, loadExpr, ConditionType.PROMPT, null,
					bindingArgs);
			addBinding(comp, attr, binding);
			final BindingKey bkey = getBindingKey(comp, attr);
			_formBindingHandler.addLoadPromptBinding(bkey, binding);

			if (collector != null) {
				collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_LOAD, comp, null, binding.getPropertyString(),
						formId, bindingArgs, null));
			}
		} else {
			if (beforeCmds != null && beforeCmds.length > 0) {
				for (String cmd : beforeCmds) {
					final LoadFormBinding binding = newLoadFormBinding(comp, formId, loadExpr,
							ConditionType.BEFORE_COMMAND, cmd, bindingArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add before command-load-form-binding: comp=[{}],attr=[{}],expr=[{}],command=[{}]",
								comp, attr, loadExpr, cmd);
					}
					_formBindingHandler.addLoadBeforeBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_LOAD, comp, "before = '" + cmd + "'",
								binding.getPropertyString(), formId, bindingArgs, null));
					}
				}
			}
			if (afterCmds != null && afterCmds.length > 0) {
				for (String cmd : afterCmds) {
					final LoadFormBinding binding = newLoadFormBinding(comp, formId, loadExpr,
							ConditionType.AFTER_COMMAND, cmd, bindingArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add after command-load-form-binding: comp=[{}],attr=[{}],expr=[{}],command=[{}]",
								comp, attr, loadExpr, cmd);
					}
					_formBindingHandler.addLoadAfterBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_LOAD, comp, "after = '" + cmd + "'",
								binding.getPropertyString(), formId, bindingArgs, null));
					}
				}
			}
		}
	}

	private void addFormSaveBindings0(Component comp, String formId, String saveExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs, String validatorExpr,
			Map validatorArgs) {
		final boolean prompt = isPrompt(beforeCmds, afterCmds);
		if (prompt) {
			throw new IllegalArgumentException(MiscUtil.formatLocationMessage(
					"a save-form-binding have to set with a before|after command condition", comp));
		}
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (beforeCmds != null && beforeCmds.length > 0) {
			for (String cmd : beforeCmds) {
				final SaveFormBinding binding = newSaveFormBinding(comp, formId, saveExpr, ConditionType.BEFORE_COMMAND,
						cmd, bindingArgs, validatorExpr, validatorArgs);
				addBinding(comp, formId, binding);
				if (_log.isDebugEnabled()) {
					_log.debug("add before command-save-form-binding: comp=[{}],attr=[{}],expr=[{}],command=[{}]", comp,
							formId, saveExpr, cmd);
				}
				_formBindingHandler.addSaveBeforeBinding(cmd, binding);

				if (collector != null) {
					collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_SAVE, comp, "before = '" + cmd + "'",
							formId, binding.getPropertyString(), bindingArgs, null));
				}
			}
		}
		if (afterCmds != null && afterCmds.length > 0) {
			for (String cmd : afterCmds) {
				final SaveFormBinding binding = newSaveFormBinding(comp, formId, saveExpr, ConditionType.AFTER_COMMAND,
						cmd, bindingArgs, validatorExpr, validatorArgs);
				addBinding(comp, formId, binding);
				if (_log.isDebugEnabled()) {
					_log.debug("add after command-save-form-binding: comp=[{}],attr=[{}],expr=[{}],command=[{}]", comp,
							formId, saveExpr, cmd);
				}
				_formBindingHandler.addSaveAfterBinding(cmd, binding);

				if (collector != null) {
					collector.addInfo(new AddBindingInfo(AddBindingInfo.FORM_SAVE, comp, "after = '" + cmd + "'",
							formId, binding.getPropertyString(), bindingArgs, null));
				}
			}
		}
		if (validatorExpr != null) {
			BindingKey bkey = getBindingKey(comp, formId);
			if (!_hasValidators.contains(bkey)) {
				_hasValidators.add(bkey);
			}
		}
	}

	public void addPropertyInitBinding(Component comp, String attr, String initExpr, Map initArgs,
			String converterExpr, Map converterArgs) {
		checkInit();
		if (initExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("initExpr is null for " + attr + " of " + comp, comp));
		}
		if (Strings.isBlank(converterExpr)) {
			converterExpr = getSystemConverter(comp, attr);
			if (converterExpr != null) {
				converterExpr = "'" + converterExpr + "'";
			}
		}

		addPropertyInitBinding0(comp, attr, initExpr, initArgs, converterExpr, converterArgs);

		initRendererIfAny(comp, attr);
	}

	public void addPropertyLoadBindings(Component comp, String attr, String loadExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs, String converterExpr,
			Map converterArgs) {
		checkInit();
		if (loadExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("loadExpr is null for component " + comp + ", attr " + attr, comp));
		}
		if (Strings.isBlank(converterExpr)) {
			converterExpr = getSystemConverter(comp, attr);
			if (converterExpr != null) {
				converterExpr = "'" + converterExpr + "'";
			}
		}

		addPropertyLoadBindings0(comp, attr, loadExpr, beforeCmds, afterCmds, bindingArgs, converterExpr,
				converterArgs);

		initRendererIfAny(comp, attr);
	}

	public void addPropertySaveBindings(Component comp, String attr, String saveExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs, String converterExpr,
			Map converterArgs, String validatorExpr, Map validatorArgs) {
		checkInit();
		if (saveExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("saveExpr is null for component " + comp + ", attr " + attr, comp));
		}
		if (Strings.isBlank(converterExpr)) {
			converterExpr = getSystemConverter(comp, attr);
			if (converterExpr != null) {
				converterExpr = "'" + converterExpr + "'";
			}
		}
		if (Strings.isBlank(validatorExpr)) {
			validatorExpr = getSystemValidator(comp, attr);
			if (validatorExpr != null) {
				validatorExpr = "'" + validatorExpr + "'";
			}
		}

		addPropertySaveBindings0(comp, attr, saveExpr, beforeCmds, afterCmds, bindingArgs, converterExpr, converterArgs,
				validatorExpr, validatorArgs);
	}

	private void addPropertyInitBinding0(Component comp, String attr, String initExpr, Map bindingArgs,
			String converterExpr, Map converterArgs) {

		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, attr);
		String loadrep = null;
		Class attrType = null; //default is any class
		if (ann != null) {
			final Map attrs = ann.getAttributes(); //(tag, tagExpr)
			loadrep = AnnotationUtil.testString(attrs.get(Binder.LOAD_REPLACEMENT), ann); //check replacement of attr when loading

			final String type = AnnotationUtil.testString(attrs.get(Binder.LOAD_TYPE), ann); //check type of attr when loading
			if (type != null) {
				try {
					attrType = Classes.forNameByThread(type);
				} catch (ClassNotFoundException e) {
					throw UiException.Aide.wrap(e, e.getMessage());
				}
			}
		}
		loadrep = loadrep == null ? attr : loadrep;

		if (_log.isDebugEnabled()) {
			_log.debug("add init-binding: comp=[{}],attr=[{}],expr=[{}],converter=[{}]", comp, attr, initExpr,
					converterArgs);
		}

		InitPropertyBinding binding = newInitPropertyBinding(comp, attr, loadrep, attrType, initExpr, bindingArgs,
				converterExpr, converterArgs);

		addBinding(comp, attr, binding);
		final BindingKey bkey = getBindingKey(comp, attr);
		_propertyBindingHandler.addInitBinding(bkey, binding);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_INIT, comp, null, binding.getPropertyString(),
					binding.getFieldName(), bindingArgs, null));
		}
	}

	private String getSystemConverter(Component comp, String attr) {
		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, attr);
		if (ann != null) {
			final Map attrs = ann.getAttributes(); //(tag, tagExpr)
			return AnnotationUtil.testString(attrs.get(Binder.CONVERTER), ann); //system converter if exists
		}
		return null;
	}

	private String getSystemValidator(Component comp, String attr) {
		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, attr);
		if (ann != null) {
			final Map attrs = ann.getAttributes(); //(tag, tagExpr)
			return AnnotationUtil.testString(attrs.get(Binder.VALIDATOR), ann); //system validator if exists
		}
		return null;
	}

	private void initRendererIfAny(Component comp, String attr) {
		final Object installed = comp.getAttribute(BinderCtrl.RENDERER_INSTALLED);
		if (installed != null) { //renderer was set already init
			return;
		}

		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, null);
		final Map attrs = ann != null ? ann.getAttributes() : null; //(tag, tagExpr)

		//only set up renderer when has model binding. (or will get error in no-model + selectedTab case
		final String installAttr = "model"; //TODO make it configurable in lang-addon.xml
		if (attrs != null && installAttr.equals(attr)) {
			final String rendererName = AnnotationUtil.testString(attrs.get(Binder.RENDERER), ann); //renderer if any
			//setup renderer
			if (rendererName != null) { //there was system renderer
				String[] values = null;
				if (rendererName.indexOf("=") != -1) {
					values = rendererName.split("=", 2); //zk 6.0.0
				} else {
					values = rendererName.split(":", 2); //after zk 6.0.1
				}

				if (values != null) {
					final Object renderer = getRenderer(values[1]);
					//check if user has set a renderer
					Object old = null;
					try {
						old = Fields.get(comp, values[0]);
					} catch (NoSuchMethodException e1) {
						//ignore
					}
					if (old == null) {
						try {
							Fields.set(comp, values[0], renderer, false);
						} catch (Exception e) {
							throw UiException.Aide.wrap(e, e.getMessage());
						}

						if (renderer instanceof TemplateRendererCtrl) {
							((TemplateRendererCtrl) renderer).setAttributeName(attr);
						}
					}

					comp.setAttribute(BinderCtrl.RENDERER_INSTALLED, ""); //mark installed
				}
			}
		}
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected LoadPropertyBinding newLoadPropertyBinding(Component comp, String attr, String loadAttr,
			Class attrType, String loadExpr, ConditionType conditionType, String command,
			Map bindingArgs, String converterExpr, Map converterArgs) {
		return new LoadPropertyBindingImpl(this, comp, attr, loadAttr, attrType, loadExpr, conditionType, command,
				bindingArgs, converterExpr, converterArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected SavePropertyBinding newSavePropertyBinding(Component comp, String attr, String saveAttr, String saveExpr,
			ConditionType conditionType, String command, Map bindingArgs, String converterExpr,
			Map converterArgs, String validatorExpr, Map validatorArgs) {
		return new SavePropertyBindingImpl(this, comp, attr, saveAttr, saveExpr, conditionType, command, bindingArgs,
				converterExpr, converterArgs, validatorExpr, validatorArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected InitPropertyBinding newInitPropertyBinding(Component comp, String attr, String loadAttr,
			Class attrType, String initExpr, Map bindingArgs, String converterExpr,
			Map converterArgs) {
		return new InitPropertyBindingImpl(this, comp, attr, loadAttr, attrType, initExpr, bindingArgs, converterExpr,
				converterArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected InitChildrenBinding newInitChildrenBinding(Component comp, String initExpr,
			Map bindingArgs, String converterExpr, Map converterArgs) {
		return new InitChildrenBindingImpl(this, comp, initExpr, bindingArgs, converterExpr, converterArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected LoadChildrenBinding newLoadChildrenBinding(Component comp, String loadExpr, ConditionType conditionType,
			String command, Map bindingArgs, String converterExpr, Map converterArgs) {
		return new LoadChildrenBindingImpl(this, comp, loadExpr, conditionType, command, bindingArgs, converterExpr,
				converterArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected CommandBinding newCommandBinding(Component comp, String evtnm, String cmdScript,
			Map args) {
		return new CommandBindingImpl(this, comp, evtnm, cmdScript, args);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected InitFormBinding newInitFormBinding(Component comp, String formId, String initExpr,
			Map bindingArgs) {
		return new InitFormBindingImpl(this, comp, formId, initExpr, bindingArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected LoadFormBinding newLoadFormBinding(Component comp, String formId, String loadExpr,
			ConditionType conditionType, String command, Map bindingArgs) {
		return new LoadFormBindingImpl(this, comp, formId, loadExpr, conditionType, command, bindingArgs);
	}

	/** Make this extenable.
	 * @since 7.0.3
	 */
	protected SaveFormBinding newSaveFormBinding(Component comp, String formId, String saveExpr,
			ConditionType conditionType, String command, Map bindingArgs, String validatorExpr,
			Map validatorArgs) {
		return new SaveFormBindingImpl(this, comp, formId, saveExpr, conditionType, command, bindingArgs, validatorExpr,
				validatorArgs);
	}

	private void addPropertyLoadBindings0(Component comp, String attr, String loadExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs, String converterExpr,
			Map converterArgs) {
		final boolean prompt = isPrompt(beforeCmds, afterCmds);
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		//check attribute _accessInfo natural characteristics to register Command event listener
		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, attr);
		//check which attribute of component should load to component on which event.
		//the event is usually a engine lifecycle event.
		//ex, listbox's 'selectedIndex' should be loaded to component on 'onAfterRender'
		String evtnm = null;
		String loadRep = null;
		Class attrType = null; //default is any class
		if (ann != null) {
			final Map attrs = ann.getAttributes(); //(tag, tagExpr)
			final String rw = AnnotationUtil.testString(attrs.get(Binder.ACCESS), ann); //_accessInfo right, "both|save|load", default to load
			if (rw != null && !"both".equals(rw) && !"load".equals(rw)) { //save only, skip
				return;
			}
			evtnm = AnnotationUtil.testString(attrs.get(Binder.LOAD_EVENT), ann); //check trigger event for loading

			loadRep = AnnotationUtil.testString(attrs.get(Binder.LOAD_REPLACEMENT), ann); //check replacement of attr when loading

			final String type = AnnotationUtil.testString(attrs.get(Binder.LOAD_TYPE), ann); //check type of attr when loading
			if (type != null) {
				try {
					attrType = Classes.forNameByThread(type);
				} catch (ClassNotFoundException e) {
					throw UiException.Aide.wrap(e, e.getMessage());
				}
			}
		}
		loadRep = loadRep == null ? attr : loadRep;

		if (prompt) {
			if (_log.isDebugEnabled()) {
				_log.debug("add event(prompt)-load-binding: comp=[{}],attr=[{}],expr=[{}],evtnm=[{}],converter=[{}]",
						comp, attr, loadExpr, evtnm, converterArgs);
			}
			LoadPropertyBinding binding = newLoadPropertyBinding(comp, attr, loadRep, attrType, loadExpr,
					ConditionType.PROMPT, null, bindingArgs, converterExpr, converterArgs);
			addBinding(comp, attr, binding);

			if (evtnm != null) { //special case, load on an event, ex, onAfterRender of listbox on selectedItem
				registerCommandEventListener(comp, evtnm); //prompt
				addBinding(comp, evtnm, binding); //to mark evtnm has a this binding, so we can remove it
				final BindingKey bkey = getBindingKey(comp, evtnm);
				_propertyBindingHandler.addLoadEventBinding(comp, bkey, binding);
			}
			//if no command , always add to prompt binding, a prompt binding will be load when , 
			//1.load a component property binding
			//2.property change (TODO, DENNIS, ISSUE, I think loading of property change is triggered by tracker in doPropertyChange, not by prompt-binding 
			final BindingKey bkey = getBindingKey(comp, attr);
			_propertyBindingHandler.addLoadPromptBinding(comp, bkey, binding);

			if (collector != null) {
				collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_LOAD, comp, evtnm, binding.getPropertyString(),
						binding.getFieldName(), bindingArgs, null));
			}
		} else {
			if (beforeCmds != null && beforeCmds.length > 0) {
				for (String cmd : beforeCmds) {
					LoadPropertyBinding binding = newLoadPropertyBinding(comp, attr, loadRep, attrType, loadExpr,
							ConditionType.BEFORE_COMMAND, cmd, bindingArgs, converterExpr, converterArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add before command-load-binding: comp=[{}],att=r[{}],expr=[{}],converter=[{}]",
								comp, attr, loadExpr, converterExpr);
					}
					_propertyBindingHandler.addLoadBeforeBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_LOAD, comp, "before = '" + cmd + "'",
								binding.getPropertyString(), binding.getFieldName(), bindingArgs, null));
					}
				}
			}
			if (afterCmds != null && afterCmds.length > 0) {
				for (String cmd : afterCmds) {
					LoadPropertyBinding binding = newLoadPropertyBinding(comp, attr, loadRep, attrType, loadExpr,
							ConditionType.AFTER_COMMAND, cmd, bindingArgs, converterExpr, converterArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add after command-load-binding: comp=[{}],att=r[{}],expr=[{}],converter=[{}]", comp,
								attr, loadExpr, converterExpr);
					}
					_propertyBindingHandler.addLoadAfterBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_LOAD, comp, "after = '" + cmd + "'",
								binding.getPropertyString(), binding.getFieldName(), bindingArgs, null));
					}
				}
			}
		}
	}

	private void addPropertySaveBindings0(Component comp, String attr, String saveExpr, String[] beforeCmds,
			String[] afterCmds, Map bindingArgs, String converterExpr,
			Map converterArgs, String validatorExpr, Map validatorArgs) {
		final boolean prompt = isPrompt(beforeCmds, afterCmds);
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		//check attribute _accessInfo natural characteristics to register Command event listener 
		final ComponentCtrl compCtrl = (ComponentCtrl) comp;
		final Annotation ann = AnnotationUtil.getSystemAnnotation(compCtrl, attr);
		//check which attribute of component should fire save on which event.
		//ex, listbox's 'selectedIndex' should be loaded to component on 'onSelect'
		//ex, checkbox's 'checked' should be saved to bean on 'onCheck'
		String evtnm = null;
		String saveRep = null;
		if (ann != null) {
			final Map attrs = ann.getAttributes(); //(tag, tagExpr)
			final String rw = AnnotationUtil.testString(attrs.get(Binder.ACCESS), ann); //_accessInfo right, "both|save|load", default to load
			if (!"both".equals(rw) && !"save".equals(rw)) { //load only, skip
				if (BinderUtil.hasContext() && BinderUtil.getContext().isIgnoreAccessCreationWarn()) {
					return;
				}
				_log.warn(MiscUtil.formatLocationMessage(
						"component " + comp + " doesn't support to save attribute " + attr, comp));
				return;
			}
			evtnm = AnnotationUtil.testString(attrs.get(Binder.SAVE_EVENT), ann); //check trigger event for saving

			saveRep = AnnotationUtil.testString(attrs.get(Binder.SAVE_REPLACEMENT), ann); //check replacement of attr when saving
		}
		if (evtnm == null) {
			//no trigger event, since the value never change of component, so both prompt and command are useless
			if (BinderUtil.hasContext() && BinderUtil.getContext().isIgnoreAccessCreationWarn()) {
				return;
			}
			_log.warn(MiscUtil
					.formatLocationMessage("component " + comp + " doesn't has event to save attribute " + attr, comp));
			return;
		}
		saveRep = saveRep == null ? attr : saveRep;

		if (prompt) {
			final SavePropertyBinding binding = newSavePropertyBinding(comp, attr, saveRep, saveExpr,
					ConditionType.PROMPT, null, bindingArgs, converterExpr, converterArgs, validatorExpr,
					validatorArgs);
			addBinding(comp, attr, binding);
			if (_log.isDebugEnabled()) {
				_log.debug(
						"add event(prompt)-save-binding: comp=[{}],attr=[{}],expr=[{}],evtnm=[{}],converter=[{}],validate=[{}]",
						comp, attr, saveExpr, evtnm, converterExpr, validatorExpr);
			}
			registerCommandEventListener(comp, evtnm); //prompt
			addBinding(comp, evtnm, binding); //to mark evtnm has a this binding, so we can remove it in removeComponent
			final BindingKey bkey = getBindingKey(comp, evtnm);
			_propertyBindingHandler.addSavePromptBinding(comp, bkey, binding);

			if (collector != null) {
				collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_SAVE, comp, null, binding.getFieldName(),
						binding.getPropertyString(), bindingArgs, null));
			}
		} else {
			if (beforeCmds != null && beforeCmds.length > 0) {
				for (String cmd : beforeCmds) {
					final SavePropertyBinding binding = newSavePropertyBinding(comp, attr, saveRep, saveExpr,
							ConditionType.BEFORE_COMMAND, cmd, bindingArgs, converterExpr, converterArgs, validatorExpr,
							validatorArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug(
								"add before command-save-binding: comp=[{}],att=r[{}],expr=[{}],converter=[{}],validator=[{}]",
								comp, attr, saveExpr, converterExpr, validatorExpr);
					}
					_propertyBindingHandler.addSaveBeforeBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_SAVE, comp, "before = '" + cmd + "'",
								binding.getFieldName(), binding.getPropertyString(), bindingArgs, null));
					}
				}
			}
			if (afterCmds != null && afterCmds.length > 0) {
				for (String cmd : afterCmds) {
					final SavePropertyBinding binding = newSavePropertyBinding(comp, attr, saveRep, saveExpr,
							ConditionType.AFTER_COMMAND, cmd, bindingArgs, converterExpr, converterArgs, validatorExpr,
							validatorArgs);
					addBinding(comp, attr, binding);
					if (_log.isDebugEnabled()) {
						_log.debug(
								"add after command-save-binding: comp=[{}],att=r[{}],expr=[{}],converter=[{}],validator=[{}]",
								comp, attr, saveExpr, converterExpr, validatorExpr);
					}
					_propertyBindingHandler.addSaveAfterBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_SAVE, comp, "after = '" + cmd + "'",
								binding.getFieldName(), binding.getPropertyString(), bindingArgs, null));
					}
				}
			}
		}

		if (validatorExpr != null) {
			BindingKey bkey = getBindingKey(comp, attr);
			if (!_hasValidators.contains(bkey)) {
				_hasValidators.add(bkey);
			}
		}
	}

	public void addChildrenInitBinding(Component comp, String initExpr, Map initArgs,
			String converterExpr, Map converterArgs) {
		checkInit();
		if (initExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("initExpr is null for children of " + comp, comp));
		}
		addChildrenInitBinding0(comp, initExpr, initArgs, converterExpr, converterArgs);
	}

	public void addChildrenLoadBindings(Component comp, String loadExpr, String[] beforeCmds, String[] afterCmds,
			Map bindingArgs, String converterExpr, Map converterArgs) {
		checkInit();
		if (loadExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("loadExpr is null for children of " + comp, comp));
		}
		addChildrenLoadBindings0(comp, loadExpr, beforeCmds, afterCmds, bindingArgs, converterExpr, converterArgs);
	}

	private void addChildrenInitBinding0(Component comp, String initExpr, Map bindingArgs,
			String converterExpr, Map converterArgs) {

		if (_log.isDebugEnabled()) {
			_log.debug("add children-init-binding: comp=[{}],expr=[{}]", comp, initExpr);
		}

		InitChildrenBinding binding = newInitChildrenBinding(comp, initExpr, bindingArgs, converterExpr, converterArgs);

		addBinding(comp, CHILDREN_ATTR, binding);
		final BindingKey bkey = getBindingKey(comp, CHILDREN_ATTR);
		_childrenBindingHandler.addInitBinding(bkey, binding);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddBindingInfo(AddBindingInfo.CHILDREN_INIT, comp, null, binding.getPropertyString(),
					null, bindingArgs, null));
		}

	}

	private void addChildrenLoadBindings0(Component comp, String loadExpr, String[] beforeCmds, String[] afterCmds,
			Map bindingArgs, String converterExpr, Map converterArgs) {
		final boolean prompt = isPrompt(beforeCmds, afterCmds);
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (prompt) {
			if (_log.isDebugEnabled()) {
				_log.debug("add event(prompt)-children-load-binding: comp=[{}],expr=[{}]", comp, loadExpr);
			}
			LoadChildrenBindingImpl binding = new LoadChildrenBindingImpl(this, comp, loadExpr, ConditionType.PROMPT,
					null, bindingArgs, converterExpr, converterArgs);
			addBinding(comp, CHILDREN_ATTR, binding);

			final BindingKey bkey = getBindingKey(comp, CHILDREN_ATTR);
			_childrenBindingHandler.addLoadPromptBinding(comp, bkey, binding);

			if (collector != null) {
				collector.addInfo(new AddBindingInfo(AddBindingInfo.CHILDREN_LOAD, comp, null,
						binding.getPropertyString(), null, bindingArgs, null));
			}

		} else {
			if (beforeCmds != null && beforeCmds.length > 0) {
				for (String cmd : beforeCmds) {
					LoadChildrenBindingImpl binding = new LoadChildrenBindingImpl(this, comp, loadExpr,
							ConditionType.BEFORE_COMMAND, cmd, bindingArgs, converterExpr, converterArgs);
					addBinding(comp, CHILDREN_ATTR, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add before command children-load-binding: comp=[{}],expr=[{}],cmd=[{}]", comp,
								loadExpr, cmd);
					}
					_childrenBindingHandler.addLoadBeforeBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.CHILDREN_LOAD, comp,
								"before = '" + cmd + "'", binding.getPropertyString(), null, bindingArgs, null));
					}
				}
			}
			if (afterCmds != null && afterCmds.length > 0) {
				for (String cmd : afterCmds) {
					LoadChildrenBindingImpl binding = new LoadChildrenBindingImpl(this, comp, loadExpr,
							ConditionType.AFTER_COMMAND, cmd, bindingArgs, converterExpr, converterArgs);
					addBinding(comp, CHILDREN_ATTR, binding);
					if (_log.isDebugEnabled()) {
						_log.debug("add after command children-load-binding: comp=[{}],expr=[{}],cmd=[{}]", comp,
								loadExpr, cmd);
					}
					_childrenBindingHandler.addLoadAfterBinding(cmd, binding);

					if (collector != null) {
						collector.addInfo(new AddBindingInfo(AddBindingInfo.CHILDREN_LOAD, comp,
								"after = '" + cmd + "'", binding.getPropertyString(), null, bindingArgs, null));
					}
				}
			}
		}
	}

	public void addReferenceBinding(Component comp, String attr, String loadExpr, Map bindingArgs) {
		checkInit();
		if (loadExpr == null) {
			throw new IllegalArgumentException(
					MiscUtil.formatLocationMessage("loadExpr is null for reference of " + comp, comp));
		}
		addReferenceBinding0(comp, attr, loadExpr, bindingArgs);
	}

	private void addReferenceBinding0(Component comp, String attr, String loadExpr, Map bindingArgs) {
		if (_log.isDebugEnabled()) {
			_log.debug("add reference-binding: comp=[{}],attr=[{}],expr=[{}]", comp, attr, loadExpr);
		}
		ReferenceBindingImpl binding = new ReferenceBindingImpl(this, comp, attr, loadExpr);

		if (_refBindingHandler != null) {
			_refBindingHandler.addReferenceBinding(comp, attr, binding);
		} else {
			throw new UiException(
					MiscUtil.formatLocationMessage("ref binding handler is not supported in current runtime.", comp));
		}

		addBinding(comp, attr, binding);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddBindingInfo(AddBindingInfo.REFERENCE, comp, null, binding.getPropertyString(),
					"self." + attr, bindingArgs, null));
		}
	}

	private boolean isPrompt(String[] beforeCmds, String[] afterCmds) {
		return (beforeCmds == null || beforeCmds.length == 0) && (afterCmds == null || afterCmds.length == 0);
	}

	public void addCommandBinding(Component comp, String evtnm, String commandExpr, Map args) {
		checkInit();
		final CommandBinding binding = newCommandBinding(comp, evtnm, commandExpr, args);
		addBinding(comp, evtnm, binding);
		registerCommandEventListener(comp, evtnm, binding, false);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddCommandBindingInfo(AddCommandBindingInfo.VIEWMODEL, comp, evtnm,
					binding.getCommandString(), args, null));
		}
	}

	public void addGlobalCommandBinding(Component comp, String evtnm, String commandExpr, Map args) {
		checkInit();
		final CommandBinding binding = newCommandBinding(comp, evtnm, commandExpr, args);
		addBinding(comp, evtnm, binding);
		registerCommandEventListener(comp, evtnm, binding, true);

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.addInfo(new AddCommandBindingInfo(AddCommandBindingInfo.GLOBAL, comp, evtnm,
					binding.getCommandString(), args, null));
		}
	}

	//associate event to CommandBinding
	private void registerCommandEventListener(Component comp, String evtnm, CommandBinding command, boolean global) {
		final CommandEventListener listener = getCommandEventListener(comp, evtnm);
		if (global) {
			listener.setGlobalCommand(command);
		} else {
			listener.setCommand(command);
		}
	}

	//associate event to prompt
	private void registerCommandEventListener(Component comp, String evtnm) {
		final CommandEventListener listener = getCommandEventListener(comp, evtnm);
		listener.setPrompt(true);
	}

	private CommandEventListener getCommandEventListener(Component comp, String evtnm) {
		final BindingKey bkey = getBindingKey(comp, evtnm);
		CommandEventListener listener = _listenerMap.get(bkey);
		if (listener == null) {
			listener = new CommandEventListener(comp);
			comp.addEventListener(evtnm, listener);
			_listenerMap.put(bkey, listener);
		}
		return listener;
	}

	private void removeEventCommandListenerIfExists(Component comp, String evtnm) {
		final BindingKey bkey = getBindingKey(comp, evtnm);
		final CommandEventListener listener = _listenerMap.remove(bkey);
		if (listener != null) {
			comp.removeEventListener(evtnm, listener);
		}
	}

	private class CommandEventListener implements EventListener, Serializable, Deferrable {
		private static final long serialVersionUID = 1L;
		//event used to trigger command
		private boolean _prompt = false;
		private CommandBinding _commandBinding;
		private CommandBinding _globalCommandBinding;
		private final Component _target;

		CommandEventListener(Component target) {
			_target = target;
		}

		private void setCommand(CommandBinding command) {
			_commandBinding = command;
		}

		private void setGlobalCommand(CommandBinding command) {
			_globalCommandBinding = command;
		}

		private void setPrompt(boolean prompt) {
			_prompt = prompt;
		}

		public void onEvent(Event event) throws Exception {
			BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
			try {
				if (collector != null) {
					collector.pushStack("ON_EVENT");
					collector.addInfo(new EventInfo(event.getTarget(), event.getName(), null));
				}
				onEvent0(event);
			} catch (Exception x) {
				_log.error(x.getMessage(), x);
				throw x;
			} finally {
				if (collector != null) {
					collector.popStack();
				}
			}
		}

		private void onEvent0(Event event) throws Exception {
			//command need to be confirmed shall be execute first!
			//must sort the command sequence?

			//BUG 619, event may come from children of some component, 
			//ex tabbox.onSelect is form tab, so we cannot depend on event's target
			final Component comp = _target; //_target is always equals _commandBinding.getComponent();
			final String evtnm = event.getName();
			final Set notifys = new LinkedHashSet();

			int cmdResult = COMMAND_SUCCESS; //command execution result, default to success
			boolean promptResult = true;
			String command = null;
			if (_log.isDebugEnabled()) {
				_log.debug("====Start command event [{}]", event);
			}
			//BUG ZK-757, The timing of saving textbox's value attribute to ViewModel is later than command execution on onChange event
			//We should save the prompt with validation first. 
			//For a prompt binding that also binds with a command, that should not be mixed with command
			//If user concern the timing of prompt save and validation with command, they should use condition not prompt  
			if (_prompt) {
				promptResult = BinderImpl.this.doSaveEvent(comp, event, notifys); //save on event
			}

			if (_commandBinding != null) {
				final BindEvaluatorX eval = getEvaluatorX();
				//ZK-3084: An EL in a command binding cannot access "event" object
				Map implicit = _implicitContributor.contirbuteCommandObject(BinderImpl.this,
						_commandBinding, event);
				BindContext ctx = new BindContextImpl(null, null, false, null, comp, null);
				ctx.setAttribute(ImplicitObjectELResolver.IMPLICIT_OBJECTS, implicit);
				command = (String) eval.getValue(ctx, comp, ((CommandBindingImpl) _commandBinding).getCommand());
				if (!Strings.isEmpty(command)) { //avoid the execution of a empty command.
					//ZK-1032 Able to wire Event to command method
					final Map args = BindEvaluatorXUtil.evalArgs(eval, comp, _commandBinding.getArgs(),
							implicit);
					cmdResult = BinderImpl.this.doCommand(comp, _commandBinding, command, event, args, notifys);
				}
			}

			//load prompt only when prompt result is success
			if (_prompt && promptResult) {
				if (_log.isDebugEnabled()) {
					_log.debug("This is a prompt command");
				}
				BinderImpl.this.doLoadEvent(comp, event); //load on event
			}

			notifyVMsgsChanged(); //always, no better way to know which properties of validation are changed

			if (_log.isDebugEnabled()) {
				_log.debug("There are [{}] property need to be notify after event = [{}], command = [{}]",
						notifys.size(), evtnm, command);
			}
			fireNotifyChanges(notifys);

			//post global command only when command success
			if (cmdResult == COMMAND_SUCCESS && _globalCommandBinding != null) {
				final BindEvaluatorX eval = getEvaluatorX();
				command = (String) eval.getValue(null, comp, ((CommandBindingImpl) _globalCommandBinding).getCommand());
				if (!Strings.isEmpty(command)) { //avoid the execution of a empty command.

					//ZK-1791 @global-command does not provide predefined "event" variable
					Map implicit = null;
					if (_implicitContributor != null) {
						implicit = _implicitContributor.contirbuteCommandObject(BinderImpl.this, _commandBinding,
								event);
					}

					final Map args = BindEvaluatorXUtil.evalArgs(eval, comp,
							_globalCommandBinding.getArgs(), implicit);
					//post global command
					postGlobalCommand(comp, _globalCommandBinding, command, event, args);
				}
			}

			if (_log.isDebugEnabled()) {
				_log.debug("====End command event [{}]", event);
			}
		}

		// ZK-2993: Provides a custom attribute to defer the event post for the specified component
		public boolean isDeferrable() {
			return "true".equals(_target.getAttribute("org.zkoss.bind.event.deferPost"));
		}
	}

	private class VMsgsChangedListener implements EventListener, Serializable {
		private static final long serialVersionUID = 1L;

		public void onEvent(Event event) throws Exception {
			if (_validationMessages != null) {
				Set notify = new HashSet();
				notify.add(new PropertyImpl(_validationMessages, ".", null));
				fireNotifyChanges(notify);
			}
		}
	}

	/**
	 * @since 6.0.1
	 */

	public boolean isActivating() {
		return _activating;
	}

	private void notifyVMsgsChanged() {
		if (_validationMessages != null) {
			//ZK-722 Validation message is not clear after form binding loaded
			//defer the validation notify as possible
			Events.postEvent(-1, _dummyTarget, new Event(ON_VMSGS_CHANGED));
		}
	}

	public int sendCommand(String command, Map args) {
		checkInit();
		final Set notifys = new HashSet();
		Event evt = null;
		//ZK-3133
		if (args != null) {
			if (args.containsKey(BinderCtrl.CLIENT_INFO)) {
				Map inf = new HashMap();
				inf.put("", args.get(BinderCtrl.CLIENT_INFO));
				evt = ClientInfoEvent.getClientInfoEvent(new AuRequest(_rootComp.getDesktop(), command, inf));
			} else {
				Event uploadInfoEvt = (Event) args.get(BinderCtrl.CLIENT_UPLOAD_INFO); // ZK-4472
				if (uploadInfoEvt != null)
					evt = uploadInfoEvt;
			}
		}
		//args come from user, we don't eval it.
		int result = doCommand(_rootComp, null, command, evt, args, notifys);
		if (result == COMMAND_FAIL_VALIDATE && _validationMessages != null) {
			notifys.add(new PropertyImpl(_validationMessages, ".", null));
		}
		fireNotifyChanges(notifys);
		return result;
	}

	protected void fireNotifyChanges(Set notifys) {
		for (Property prop : notifys) {
			notifyChange(prop.getBase(), prop.getProperty());
		}
	}

	public void postCommand(String command, Map args) {
		checkInit();
		final Event evt = new Event(ON_POST_COMMAND, _dummyTarget, new Object[] { command, args });
		Events.postEvent(evt);
	}

	/**
	 * @param comp the component that trigger the command, major life cycle of binding (on event trigger)
	 * @param commandBinding the command binding, nullable
	 * @param command command is the command name after evaluation
	 * @param evt event that fire this command, nullable
	 * @param commandArgs the passed in argument for executing command
	 * @param notifys container for properties that is to be notifyChange
	 * @return the result of the doCommand, COMMAND_SUCCESS or COMMAND_FAIL_VALIDATE 
	 */
	private int doCommand(Component comp, CommandBinding commandBinding, String command, Event evt,
			Map commandArgs, Set notifys) {
		final String evtnm = evt == null ? null : evt.getName();
		String debugInfo = MessageFormat.format("doCommand "
				+ "comp=[{0}],command=[{1}],evtnm=[{2}]", comp, command, evtnm);
		if (_log.isDebugEnabled()) {
			_log.debug("Start " + debugInfo);
		}
		BindContext ctx = BindContextUtil.newBindContext(this, commandBinding, false, command, comp, evt);
		BindContextUtil.setCommandArgs(this, comp, ctx, commandArgs);
		try {
			doPrePhase(Phase.COMMAND, ctx); //begin of Command
			boolean success = true;

			final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
			if (collector != null) {
				collector.addInfo(new CommandInfo(CommandInfo.ON_COMMAND, comp, evtnm,
						commandBinding == null ? null
								: BindEvaluatorXUtil
										.getExpressionString(((CommandBindingImpl) commandBinding).getCommand()),
						command, commandArgs, null));
			}

			//validate
			success = doValidate(comp, command, evt, ctx, notifys);
			if (!success) {
				return COMMAND_FAIL_VALIDATE;
			}

			//save before command bindings
			doSaveBefore(comp, command, evt, ctx, notifys);

			//load before command bindings
			doLoadBefore(comp, command, ctx);

			//execute command
			doExecute(comp, command, commandArgs, ctx, notifys);

			//save after command bindings
			doSaveAfter(comp, command, evt, ctx, notifys);

			//load after command bindings
			doLoadAfter(comp, command, ctx);
			if (_log.isDebugEnabled()) {
				_log.debug("End doCommand");
			}
			return COMMAND_SUCCESS;
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.COMMAND, ctx); //end of Command
		}

	}

	private void doGlobalCommand(Component comp, String command, Event evt, Map commandArgs,
			Set notifys) {
		String debugInfo = MessageFormat.format("doGlobalCommand comp=[{0}],command=[{1}]", comp, command);
		if (_log.isDebugEnabled()) {
			_log.debug("Start " + debugInfo);
		}

		BindContext ctx = BindContextUtil.newBindContext(this, null, false, command, comp, evt);
		BindContextUtil.setCommandArgs(this, comp, ctx, commandArgs);
		try {
			doPrePhase(Phase.GLOBAL_COMMAND, ctx); //begin of Command

			final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
			if (collector != null) {
				collector.addInfo(
						new CommandInfo(CommandInfo.ON_GLOBAL_COMMAND, comp, null, null, command, commandArgs, null));
			}

			//execute command
			doGlobalCommandExecute(comp, command, commandArgs, ctx, notifys);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.GLOBAL_COMMAND, ctx); //end of Command
		}
	}

	private void doGlobalCommandExecute(Component comp, String command, Map commandArgs,
			BindContext ctx, Set notifys) {
		String debugInfo = MessageFormat.format("doGlobalCommandExecute comp=[{0}],command=[{1}]", comp, command); 
		try {
			if (_log.isDebugEnabled()) {
				_log.debug("before " + debugInfo);
			}
			doPrePhase(Phase.EXECUTE, ctx);

			final Object viewModel = getViewModelInView();

			Method method = getCommandMethod(BindUtils.getViewModelClass(viewModel), command, _globalCommandMethodInfoProvider,
					_globalCommandMethodCache, commandArgs != null ? commandArgs.size() : 0, true);

			if (method != null) {

				BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
				if (collector != null) {
					collector.addInfo(new CommandInfo(CommandInfo.EXECUTE_GLOBAL, comp, null, null, command,
							commandArgs, method.toString()));
				}

				ParamCall parCall = createParamCall(ctx);
				if (commandArgs != null) {
					parCall.setBindingArgs(commandArgs);
				}
				handleNotifyChange(ctx, viewModel, method, parCall, notifys);
			} else {
				//do nothing
				if (_log.isDebugEnabled()) {
					_log.debug("no global command method in [{}]", viewModel);
				}
				debugInfo += MessageFormat.format(",no global command method in viewModel=[{0}]", viewModel);
			}
			if (_log.isDebugEnabled()) {
				_log.debug("after doGlobalCommandExecute notifys=[{}]", notifys);
			}
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.EXECUTE, ctx);
		}
	}

	static void handleNotifyChange(BindContext ctx, Object viewModel,
	                                         Method method, ParamCall parCall,
	                                         Set notifys) {
		final SmartNotifyChange sannt = ViewModelAnnotationResolvers.getAnnotation(method, SmartNotifyChange.class);
		Object originViewModel = getOriginViewModel(viewModel);
		if (sannt != null) {
			Set properties = new LinkedHashSet(5);
			properties.addAll(BindELContext.getNotifys(method, originViewModel, (String) null, (Object) null, ctx)); // collect notifyChange

			parCall.call(viewModel, method);

			for (Iterator it = properties.iterator(); it.hasNext();) {
				Property prop = it.next();
				Object result = null;
				try {
					result = Fields.get(prop.getBase(), prop.getProperty());
					if (Objects.equals(result, prop.getValue()))
						it.remove();

				} catch (NoSuchMethodException ignored) {
				}
			}
			notifys.addAll(properties);
		} else {
			parCall.call(viewModel, method);
			notifys.addAll(BindELContext.getNotifys(method, originViewModel, (String) null, (Object) null, ctx)); // collect notifyChange
		}
	}

	/*package*/ void doPrePhase(Phase phase, BindContext ctx) {
		BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.pushStack(phase.name());
		}
		for (PhaseListener listener : getPhaseListeners()) {
			if (listener != null) {
				listener.prePhase(phase, ctx);
			}
		}
	}

	/*package*/ void doPostPhase(Phase phase, BindContext ctx) {
		for (PhaseListener listener : getPhaseListeners()) {
			if (listener != null) {
				listener.postPhase(phase, ctx);
			}
		}
		BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		if (collector != null) {
			collector.popStack();
		}
	}

	//for event -> prompt only, no command 
	private boolean doSaveEvent(Component comp, Event evt, Set notifys) {
		final String evtnm = evt == null ? null : evt.getName();
		if (_log.isDebugEnabled()) {
			_log.debug("doSaveEvent comp=[{}],evtnm=[{}],notifys=[{}]", comp, evtnm, notifys);
		}
		final BindingKey bkey = getBindingKey(comp, evtnm);
		return _propertyBindingHandler.doSaveEvent(bkey, comp, evt, notifys);
	}

	//for event -> prompt only, no command
	private void doLoadEvent(Component comp, Event evt) {
		if (_log.isDebugEnabled()) {
			_log.debug("doLoadEvent comp=[{}],evtnm=[{}]", comp, evt.getName());
		}
		final BindingKey bkey = getBindingKey(comp, evt.getName());
		_propertyBindingHandler.doLoadEvent(bkey, comp, evt);
	}

	//doCommand -> doValidate
	protected boolean doValidate(Component comp, String command, Event evt, BindContext ctx, Set notifys) {
		final Set validates = new HashSet();
		String debugInfo = MessageFormat.format("doValidate "
				+ "comp=[{0}],command=[{1}],evt=[{2}],context=[{3}]", comp, command, evt, ctx);
		try {
			if (_log.isDebugEnabled()) {
				_log.debug(debugInfo);
			}
			doPrePhase(Phase.VALIDATE, ctx);

			//we collect properties that need to be validated, than validate one-by-one
			ValidationHelper vHelper = new ValidationHelper(this, new ValidationHelper.InfoProvider() {
				public Map> getSaveFormBeforeBindings() {
					return _formBindingHandler.getSaveFormBeforeBindings();
				}

				public Map> getSaveFormAfterBindings() {
					return _formBindingHandler.getSaveFormAfterBindings();
				}

				public Map> getSaveBeforeBindings() {
					return _propertyBindingHandler.getSaveBeforeBindings();
				}

				public Map> getSaveAfterBindings() {
					return _propertyBindingHandler.getSaveAfterBindings();
				}

				public BindingKey getBindingKey(Component comp, String attr) {
					return BinderImpl.this.getBindingKey(comp, attr);
				}
			});

			//collect Property of special command for validation in validates
			vHelper.collectSaveBefore(comp, command, evt, validates);
			vHelper.collectSaveAfter(comp, command, evt, validates);

			//do validation (defined by application)
			if (validates.isEmpty()) {
				return true;
			} else {
				if (_log.isDebugEnabled()) {
					_log.debug("doValidate validates=[{}]", validates);
				}
				debugInfo += MessageFormat.format(",validates=[{0}]", validates);
				boolean valid = true;

				//ZK-878 Exception if binding a form with errorMessage
				//To handle wrong value exception when getting a component value.
				for (Property p : validates) {
					if (p instanceof WrongValuePropertyImpl) {
						for (WrongValueException wve : ((WrongValuePropertyImpl) p).getWrongValueExceptions()) {
							//refer to UiEngineImpl#handleError()
							Component wvc = wve.getComponent();
							if (wvc != null) {
								wve = ((ComponentCtrl) wvc).onWrongValue(wve);
								if (wve != null) {
									Component c = wve.getComponent();
									if (c == null)
										c = wvc;
									Clients.wrongValue(c, wve.getMessage());
								}
							}
						}
						valid = false;
					}
				}

				Map properties = _propertyBindingHandler.toCollectedProperties(validates);
				valid &= vHelper.validateSaveBefore(comp, command, properties, valid, notifys);
				valid &= vHelper.validateSaveAfter(comp, command, properties, valid, notifys);

				return valid;
			}
		} catch (Exception e) {
			_log.error(debugInfo, e);
			throw UiException.Aide.wrap(e, e.getMessage());
		} finally {
			doPostPhase(Phase.VALIDATE, ctx);
		}
	}

	protected ParamCall createParamCall(BindContext ctx) {
		final ParamCall call = new ParamCall();
		call.setBinder(this);
		call.setBindContext(ctx);
		final Component comp = ctx.getComponent();
		if (comp != null) {
			call.setComponent(comp);
		}
		final Execution exec = Executions.getCurrent();
		if (exec != null) {
			call.setExecution(exec);
		}

		return call;
	}

	protected void doExecute(Component comp, String command, Map commandArgs, BindContext ctx,
			Set notifys) {
		String debugInfo = MessageFormat.format("doExecute "
				+ "comp=[{0}],command=[{1}],notifys=[{2}]", comp, command, notifys);
		try {
			Matcher matcher = CALL_OTHER_VM_COMMAND_PATTERN.matcher(command);
			if (matcher.find()) {
				String vmId = matcher.group(1);
				Map vmIdBinderMap = (Map) comp.getDesktop().getAttribute(BinderCtrl.VIEWMODELID_BINDER_MAP_KEY);
				Binder targetBinder = vmIdBinderMap.get(vmId);
				if (targetBinder != null) {
					((BinderImpl) targetBinder).doExecute(comp, command.replace("$" + vmId + ".", ""), commandArgs, ctx, notifys);
					return;
				}
			}
			if (_log.isDebugEnabled()) {
				_log.debug("before " + debugInfo);
			}
			doPrePhase(Phase.EXECUTE, ctx);

			final Object viewModel = getViewModelInView();
			Class viewModelClass = BindUtils.getViewModelClass(viewModel);

			Method method = getCommandMethod(viewModelClass, command, _commandMethodInfoProvider,
					_commandMethodCache, commandArgs != null ? commandArgs.values().size() : 0, false);

			if (method != null) {

				BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
				if (collector != null) {
					collector.addInfo(new CommandInfo(CommandInfo.EXECUTE, comp, null, null, command, commandArgs,
							method.toString()));
				}

				ParamCall parCall = createParamCall(ctx);
				if (commandArgs != null) {
					parCall.setBindingArgs(commandArgs);
				}
				handleNotifyChange(ctx, viewModel, method, parCall, notifys);
			} else if (_notifyCommands == null || !_notifyCommands.containsKey(command)) {

				// F80-ZK-2951, ignore starting with ':' and '/'
				if (!(command.startsWith(":") || command.startsWith("/")))
					throw new UiException(
							MiscUtil.formatLocationMessage("cannot find any method that is annotated for the command "
									+ command + " with @Command in " + viewModel, comp));
			}
			if (_log.isDebugEnabled()) {
				_log.debug("after doExecute notifys=[{}]", notifys);
			}
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.EXECUTE, ctx);
		}
	}

	private static interface CommandMethodInfoProvider {
		String getAnnotationName();

		String getDefaultAnnotationName();

		String[] getCommandName(Method method);

		boolean isDefaultMethod(Method m);

	}

	private Method getCommandMethod(Class clz, String command, CommandMethodInfoProvider cmdInfo,
			Map, Map>> cache, int commandParamCount, boolean isGlobal) {
		Map> methods;
		Method matchedMethodWithoutAnno = null;
		CachedItem method = null;
		synchronized (cache == _commandMethodCache ? _commandMethodCache : _globalCommandMethodCache) {
			methods = cache.computeIfAbsent(clz,
					k -> new HashMap<>());
			method = methods.get(command);
			boolean inited = false;
			if (method != null) { //quick check and return
				return method.value;
			} else if (methods.get(COMMAND_METHOD_MAP_INIT) != null) {
				//map is already initialized
				inited = true;
			}
			//scan
			for (Method m : clz.getMethods()) {
				if (m.isBridge()) continue;
				String mName = m.getName();
				if (!isGlobal && mName.equals(command) && m.getParameterTypes().length == commandParamCount)
					matchedMethodWithoutAnno = m;
				if (inited) continue; //already scanned @Default and @Command
				if (cmdInfo.isDefaultMethod(m)) {
					if (methods.get(COMMAND_METHOD_DEFAULT) != null) {
						throw new UiException("there are more than one " + cmdInfo.getDefaultAnnotationName()
								+ " method in " + clz + ", " + methods.get(COMMAND_METHOD_DEFAULT).value + " and " + m);
					}
					methods.put(COMMAND_METHOD_DEFAULT, new CachedItem(m));
				}
				String[] vals = cmdInfo.getCommandName(m);
				if (vals == null)
					continue;
				if (vals.length == 0) {
					vals = new String[] {mName}; //command name from method.
				}
				for (String val : vals) {
					val = val.trim();
					if (methods.get(val) != null) {
						throw new UiException("there are more than one " + cmdInfo.getAnnotationName() + " method "
								+ val + " in " + clz + ", " + methods.get(val).value + " and " + m);
					}
					methods.put(val, new CachedItem(m));
				}
			}

			if (!inited) {
				//ZK-3133 for matchMedia methods cache
				if (_matchMediaValues != null) {
					for (Map.Entry entry : _matchMediaValues.entrySet()) {
						methods.put(entry.getKey(), new CachedItem(entry.getValue()));
					}
				}
				methods.put(COMMAND_METHOD_MAP_INIT, NULL_METHOD); //mark this map has been initialized.
			}

			// ZK-4552
			method = methods.get(command);
			if (method == null) {
				if (matchedMethodWithoutAnno != null) {
					method = new CachedItem(matchedMethodWithoutAnno);
					methods.put(command, method);
				} else {
					method = methods.get(COMMAND_METHOD_DEFAULT); //get default
					if (method != null)
						methods.put(command, method);
				}
			}
		}
		return method == null ? null : method.value;
	}

	//doCommand -> doSaveBefore
	protected void doSaveBefore(Component comp, String command, Event evt, BindContext ctx, Set notifys) {
		String debugInfo = MessageFormat.format("doSaveBefore "
				+ "comp=[{0}],command=[{1}],evt=[{2}],notifys=[{3}]", comp, command, evt, notifys);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}
		try {
			doPrePhase(Phase.SAVE_BEFORE, ctx);
			_propertyBindingHandler.doSaveBefore(comp, command, evt, notifys);
			_formBindingHandler.doSaveBefore(comp, command, evt, notifys);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.SAVE_BEFORE, ctx);
		}
	}

	protected void doSaveAfter(Component comp, String command, Event evt, BindContext ctx, Set notifys) {
		String debugInfo = MessageFormat.format("doSaveAfter "
				+ "comp=[{0}],command=[{1}],evt=[{2}],notifys=[{3}]", comp, command, evt, notifys);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}
		try {
			doPrePhase(Phase.SAVE_AFTER, ctx);
			_propertyBindingHandler.doSaveAfter(comp, command, evt, notifys);
			_formBindingHandler.doSaveAfter(comp, command, evt, notifys);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.SAVE_AFTER, ctx);
		}

	}

	protected void doLoadBefore(Component comp, String command, BindContext ctx) {
		String debugInfo = MessageFormat.format("doLoadBefore comp=[{0}],command=[{1}]", comp, command);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}
		try {
			doPrePhase(Phase.LOAD_BEFORE, ctx);
			_propertyBindingHandler.doLoadBefore(comp, command);
			_formBindingHandler.doLoadBefore(comp, command);
			_childrenBindingHandler.doLoadBefore(comp, command);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.LOAD_BEFORE, ctx);
		}
	}

	protected void doLoadAfter(Component comp, String command, BindContext ctx) {
		String debugInfo = MessageFormat.format("doLoadAfter comp=[{0}],command=[{1}]", comp, command);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}
		try {
			doPrePhase(Phase.LOAD_AFTER, ctx);
			_propertyBindingHandler.doLoadAfter(comp, command);
			_formBindingHandler.doLoadAfter(comp, command);
			_childrenBindingHandler.doLoadAfter(comp, command);
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex);
		} finally {
			doPostPhase(Phase.LOAD_AFTER, ctx);
		}

	}

	public void removeBindings(Set comps) {
		for (Component comp : comps) {
			removeBindings0(comp);
		}
		//remove tracking batchly
		TrackerImpl tracker = (TrackerImpl) getTracker();
		tracker.removeTrackings(comps);
	}

	/**
	 * Remove all bindings that associated with the specified component.
	 * @param comp the component
	 */
	public void removeBindings(Component comp) {
		removeBindings0(comp);
		//remove tracking
		TrackerImpl tracker = (TrackerImpl) getTracker();
		tracker.removeTrackings(comp);
	}
	
	private void removeBindings(Collection removed, Component comp) {
		_formBindingHandler.removeBindings(removed);
		_propertyBindingHandler.removeBindings(removed);
		_childrenBindingHandler.removeBindings(removed);
	}
	
	/**
	 * Remove all bindings that associated with the specified component and key (_fieldExpr|evtnm|formid).
	 * @param comp the component
	 * @param key can be component attribute, event name, or form id
	 */
	public void removeBindings(Component comp, String key) {
		checkInit();
		removeEventCommandListenerIfExists(comp, key); //_listenerMap; //comp+evtnm -> eventlistener
		
		final BindingKey bkey = getBindingKey(comp, key);
		final Set removed = new HashSet();
		
		_formBindingHandler.removeBindings(bkey, removed);
		_propertyBindingHandler.removeBindings(bkey, removed);
		_childrenBindingHandler.removeBindings(bkey, removed);
		if (_validationMessages != null) {
			_validationMessages.clearMessages(comp, key);
		}
		_hasValidators.remove(bkey);
		
		removeTemplateResolver(comp, key);
		
		if (_refBindingHandler != null) {
			_refBindingHandler.removeReferenceBinding(comp, key);
		}
		
		//F80 - store subtree's binder annotation count
		removeBindings(removed, comp);
	}

	private void removeBindings0(Component comp) {
		checkInit();
		if (_rootComp == comp) {
			//the binder component was detached, unregister queue
			unsubscribeQueue(_quename, _quescope, _queueListener);
			_rootComp.removeAttribute(ACTIVATOR);
		}
		if (_validationMessages != null) {
			_validationMessages.clearMessages(comp);
		}

		final Map> attrMap = _bindings.remove(comp);
		if (attrMap != null) {
			final Set removed = new HashSet();
			for (Entry> entry : attrMap.entrySet()) {
				final String key = entry.getKey();
				removeBindings(comp, key);
				removed.addAll(entry.getValue());
			}
			if (!removed.isEmpty()) {
				removeBindings(removed, comp);
			}
		}

		removeFormAssociatedSaveBinding(comp);
		removeForm(comp);

		removeTemplateResolver(comp);
		if (_refBindingHandler != null) {
			_refBindingHandler.removeReferenceBinding(comp);
		}

		BinderUtil.unmarkHandling(comp);

		// if it is a nested binder, we have to put a mark for it and re-bind it
		// when it is re-attached again.
		if (comp.hasAttribute(BindComposer.BINDER_ID))
			comp.setAttribute(REMOVE_BINDINGS, Boolean.TRUE);
	}

	public List getLoadPromptBindings(Component comp, String attr) {
		checkInit();
		final List bindings = new ArrayList();
		final BindingKey bkey = getBindingKey(comp, attr);
		final List loadBindings = _propertyBindingHandler.getLoadPromptBindings(bkey);
		if (loadBindings != null && loadBindings.size() > 0) {
			bindings.addAll(loadBindings);
		}
		if (bindings.size() == 0) { //optimize, they are exclusive
			List childrenLoadBindings = _childrenBindingHandler.getLoadPromptBindings(bkey);
			if (childrenLoadBindings != null && childrenLoadBindings.size() > 0) {
				bindings.addAll(childrenLoadBindings);
			}
		}
		return bindings;
	}

	private void addBinding(Component comp, String attr, Binding binding) {
		//ZK-2289: Futher optimize zkbind memory consumption.
		Map> attrMap = _bindings.get(comp);
		List bindings = attrMap == null ? null : attrMap.get(attr);
		bindings = AllocUtil.inst.addList(bindings, binding);
		//bug 657, we have to keep the attribute assignment order.
		attrMap = AllocUtil.inst.putLinkedHashMap(attrMap, attr, bindings);
		_bindings.put(comp, attrMap);

		//associate component with this binder, which means, one component can only bind by one binder
		BinderUtil.markHandling(comp, this);
	}

	public void setTemplate(Component comp, String attr, String templateExpr, Map templateArgs) {
		Map resolvers = _templateResolvers.get(comp);
		if (resolvers == null) {
			resolvers = new HashMap();
			_templateResolvers.put(comp, resolvers);
		}
		resolvers.put(attr, newTemplateResolverImpl(this, comp, attr, templateExpr, templateArgs));
	}

	//ZK-1787 When the viewModel tell binder to reload a list, the other component that bind a bean in the list will reload again
	@SuppressWarnings("unchecked")
	private TemplateResolver newTemplateResolverImpl(BinderImpl binderImpl, Component comp, String attr,
			String templateExpr, Map templateArgs) {
		String clznm = Library.getProperty("org.zkoss.bind.TemplateResolver.class",
				TemplateResolverImpl.class.getName());
		try {
			Class clz = (Class) Classes.forNameByThread(clznm);
			Constructor c = clz.getDeclaredConstructor(Binder.class, Component.class, String.class,
					String.class, Map.class);
			TemplateResolver resolver = c.newInstance(binderImpl, comp, attr, templateExpr, templateArgs);
			return resolver;
		} catch (Exception e) {
			throw UiException.Aide.wrap(e, "Can't initialize template resolver ");
		}
	}

	public TemplateResolver getTemplateResolver(Component comp, String attr) {
		Map resolvers = _templateResolvers.get(comp);
		return resolvers == null ? null : resolvers.get(attr);
	}

	private void removeTemplateResolver(Component comp, String attr) {
		Map resolvers = _templateResolvers.get(comp);
		if (resolvers != null) {
			resolvers.remove(attr);
		}
	}

	private void removeTemplateResolver(Component comp) {
		_templateResolvers.remove(comp);
	}

	@SuppressWarnings("unchecked")
	public Tracker getTracker() {
		if (_tracker == null) {
			String clznm = Library.getProperty("org.zkoss.bind.Tracker.class");
			if (clznm != null) {
				Class clz;
				try {
					clz = (Class) Classes.forNameByThread(clznm);
					_tracker = clz.newInstance();
				} catch (Exception e) {
					throw UiException.Aide.wrap(e, "Can't initialize tracker");
				}
			} else
				_tracker = new TrackerImpl();
		}
		return _tracker;
	}

	/**
	 * Internal Use only. init and load the component
	 */
	public void loadComponent(Component comp, boolean loadinit) {
		loadComponent0(comp, loadinit);
		if (comp == getView() && _notifyCommands != null) {
			// init notifyCommands here
			initNotifyCommands(comp);
		}
	}

	private void initNotifyCommands(Component comp) {
		for (Map.Entry me : _notifyCommands.entrySet()) {
			addPropertyLoadBindings4Command(comp, me.getValue().onChange(), me.getKey());
		}
	}

	// since 8.0.0
	private Map _dynamicAttrs = new HashMap(5) {
		public Object put(final String key, Object value) {
			Object oldValue = super.put(key, null); // yes, we put "null" to save the memory
			BinderImpl.this.postCommand(key, Collections.singletonMap("", value));
			return oldValue;
		}
	};

	/**
	 * Internal use only.
	 * @since 8.0.0
	 */
	public Map getDynamicAttrs() {
		return _dynamicAttrs;
	}

	/**
	 * Internal use only.
	 * @since 8.0.0
	 */
	public void setDynamicAttrs(String command, Object value) {
		_dynamicAttrs.put(command, value);
	}

	private void addPropertyLoadBindings4Command(Component comp, String loadExpr, String command) {
		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		final String attr = "dynamicAttrs['" + command + "']";
		String vmname = (String) comp.getAttribute(BindComposer.VM_ID);
		if (vmname != null)
			loadExpr = loadExpr.replaceAll("_vm_", vmname);

		final String loadAttr = "attributes['" + BinderCtrl.BINDER + "']." + attr;
		LoadPropertyBinding binding = newLoadPropertyBinding(comp, attr, loadAttr, null, loadExpr, ConditionType.PROMPT,
				null, null, null, null);
		addBinding(comp, attr, binding);

		//if no command , always add to prompt binding, a prompt binding will be load when , 
		//1.load a component property binding
		//2.property change (TODO, DENNIS, ISSUE, I think loading of property change is triggered by tracker in doPropertyChange, not by prompt-binding 
		final BindingKey bkey = getBindingKey(comp, attr);
		_propertyBindingHandler.addLoadPromptBinding(comp, bkey, binding);

		if (collector != null) {
			collector.addInfo(new AddBindingInfo(AddBindingInfo.PROP_LOAD, comp, "", binding.getPropertyString(),
					binding.getFieldName(), null, null));
		}

		_propertyBindingHandler.doLoad(comp, bkey);
	}

	protected void loadComponent0(Component comp, boolean loadinit) {
		loadComponentProperties0(comp, loadinit);

		final Map> compBindings = _bindings.get(comp);
		if (_activating || compBindings == null || !compBindings.keySet().contains(CHILDREN_ATTR)) {
			for (Component kid = comp.getFirstChild(); kid != null; kid = kid.getNextSibling()) {
				loadComponent0(kid, loadinit); //recursive
			}

			// Bug ZK-3046, we handle it in ShadowElementsCtrl.filterOutShadows() when invoked by BindChildRenderer.java
			if (comp instanceof ComponentCtrl) {
				for (ShadowElement se : ((ComponentCtrl) comp).getShadowRoots()) {
					loadComponent0((Component) se, loadinit); //recursive
				}
			}
		}
	}

	private void loadComponentProperties0(Component comp, boolean loadinit) {

		final Map> compBindings = _bindings.get(comp);
		if (compBindings != null) { // if component is not registered in this binder, do nothing.
			for (String key : compBindings.keySet()) {
				final BindingKey bkey = getBindingKey(comp, key);
				if (loadinit) {
					_formBindingHandler.doInit(comp, bkey);
				}
				_formBindingHandler.doLoad(comp, bkey);
			}
			for (String key : compBindings.keySet()) {
				final BindingKey bkey = getBindingKey(comp, key);
				if (loadinit) {
					_propertyBindingHandler.doInit(comp, bkey);
				}
				_propertyBindingHandler.doLoad(comp, bkey);
			}
			for (String key : compBindings.keySet()) {
				final BindingKey bkey = getBindingKey(comp, key);
				if (loadinit) {
					_childrenBindingHandler.doInit(comp, bkey);
				}

				_childrenBindingHandler.doLoad(comp, bkey);
			}
		}
	}

	public void notifyChange(Object base, String attr) {
		checkInit();
		if (_log.isDebugEnabled()) {
			_log.debug("notifyChange base=[{}],attr=[{}]", base, attr);
		}
		getEventQueue().publish(new PropertyChangeEvent(_rootComp, base, attr));
	}

	private void postGlobalCommand(Component comp, CommandBinding commandBinding, String command, Event evt,
			Map args) {
		String debugInfo = MessageFormat.format("postGlobalCommand command=[{0}],args=[{1}]", command, args);
		if (_log.isDebugEnabled()) {
			_log.debug(debugInfo);
		}

		final BindingExecutionInfoCollector collector = getBindingExecutionInfoCollector();
		try {
			if (collector != null) {
				collector.pushStack("POST_GLOBAL_COMMAND");
				collector.addInfo(new CommandInfo(CommandInfo.POST_GLOBAL, comp, evt == null ? null : evt.getName(),
						BindEvaluatorXUtil.getExpressionString(((CommandBindingImpl) commandBinding).getCommand()),
						command, args, null));
			}

			getEventQueue().publish(new GlobalCommandEvent(_rootComp, command, args, evt));
		} catch (Exception ex) {
			throw new RuntimeException(debugInfo, ex); 
		} finally {
			if (collector != null) {
				collector.popStack();
			}
		}
	}

	public void setPhaseListener(PhaseListener listener) {
		addPhaseListener(listener);
	}

	public void addPhaseListener(PhaseListener listener) {
		_phaseListeners.add(listener);
	}

	public PhaseListener getPhaseListener() {
		List list = getPhaseListeners();
		if (list != null && !list.isEmpty())
			return list.get(0);
		return null;
	}

	public List getPhaseListeners() {
		return _phaseListeners;
	}

	private void subscribeQueue(String quename, String quescope, EventListener listener) {
		EventQueue que = EventQueues.lookup(quename, quescope, true);
		que.subscribe(listener);
	}

	private void unsubscribeQueue(String quename, String quescope, EventListener listener) {
		EventQueue que = EventQueues.lookup(quename, quescope, false);
		if (que != null) {
			que.unsubscribe(listener);
		}
	}

	private boolean isSubscribed(String quename, String quescope, EventListener listener) {
		EventQueue que = EventQueues.lookup(quename, quescope, false);
		return que == null ? false : que.isSubscribed(listener);
	}

	protected EventQueue getEventQueue() {
		return EventQueues.lookup(_quename, _quescope, true);
	}

	// create a unique id base on component's uuid and attr
	private BindingKey getBindingKey(Component comp, String attr) {
		return new BindingKey(comp, attr);
	}

	private class PostCommandListener implements EventListener, Serializable {
		private static final long serialVersionUID = 1L;

		@SuppressWarnings("unchecked")
		public void onEvent(Event event) throws Exception {
			Object[] data = (Object[]) event.getData();
			String command = (String) data[0];
			Map args = (Map) data[1];
			sendCommand(command, args);
		}
	}

	private void removeFormAssociatedSaveBinding(Component comp) {
		_assocFormSaveBindings.remove(comp);
		Map> associated = _reversedAssocFormSaveBindings.remove(comp);
		if (associated != null) {
			Set>> entries = associated.entrySet();
			for (Entry> entry : entries) {
				entry.getValue().remove(entry.getKey());
			}
		}
	}

	public void addFormAssociatedSaveBinding(Component associatedComp, String formId, SaveBinding saveBinding,
			String fieldName) {
		checkInit();
		//find the form component by form id and a associated/nested component
		Component formComp = lookupAssociatedFormComponent(formId, associatedComp);
		if (formComp == null) {
			throw new UiException("cannot find any form " + formId + " with " + associatedComp);
		}
		Binder saveCompBinder = saveBinding.getBinder();
		boolean isSameBinder = this.equals(saveCompBinder);
		Set bindings = null;
		Set originalBindings = _assocFormSaveBindings.get(formComp);
		if (!isSameBinder) {
			bindings = ((BinderImpl) saveCompBinder)._assocFormSaveBindings.get(formComp);
			if (bindings == null) {
				bindings = new LinkedHashSet();
				((BinderImpl) saveCompBinder)._assocFormSaveBindings.put(formComp, bindings);
			}
			if (originalBindings != null)
				bindings.addAll(originalBindings);
		} else if (bindings == null) {
			bindings = originalBindings != null ? originalBindings : new LinkedHashSet(); //keep the order
		}
		_assocFormSaveBindings.put(formComp, bindings);
		bindings.add(saveBinding);

		//keep the reverse association , so we can remove it if the associated component is detached (and the form component is not).
		Map> reverseMap = null;
		Map> originalReverseMap = _reversedAssocFormSaveBindings.get(associatedComp);
		if (!isSameBinder) {
			reverseMap = ((BinderImpl) saveCompBinder)._reversedAssocFormSaveBindings.get(associatedComp);
			if (reverseMap == null) {
				reverseMap = new HashMap>();
				((BinderImpl) saveCompBinder)._reversedAssocFormSaveBindings.put(associatedComp, reverseMap);
			}
			if (originalReverseMap != null)
				reverseMap.get(saveBinding).addAll(originalReverseMap.get(saveBinding));
		} else if (reverseMap == null) {
			reverseMap = originalReverseMap != null ? originalReverseMap : new HashMap>();
		}
		_reversedAssocFormSaveBindings.put(associatedComp, reverseMap);
		reverseMap.put(saveBinding, bindings);

		//ZK-1017 Property of a form is not correct when validation
		//ZK-1005 ZK 6.0.1 validation fails on nested bean
		((SavePropertyBindingImpl) saveBinding).setFormFieldInfo(formComp, formId, fieldName);
	}

	private Component lookupAssociatedFormComponent(String formId, Component associatedComp) {
		String fid = null;
		Component p = associatedComp;
		while (p != null) {
			fid = (String) p.getAttribute(FORM_ID); //check in default component scope
			if (fid != null && fid.equals(formId)) {
				break;
			}
			p = p.getParent();
		}

		return p;
	}

	public Set getFormAssociatedSaveBindings(Component comp) {
		checkInit();
		Set bindings = _assocFormSaveBindings.get(comp);
		if (bindings == null) {
			return Collections.emptySet();
		}
		return new LinkedHashSet(bindings); //keep the order
	}

	//utility to simply hold a value which might be null
	private static class CachedItem {
		final T value;

		public CachedItem(T value) {
			this.value = value;
		}
	}

	public boolean hasValidator(Component comp, String attr) {
		BindingKey bkey = getBindingKey(comp, attr);
		return _hasValidators.contains(bkey);
	}

	public Component getView() {
		checkInit();
		return _rootComp;
	}

	public ValidationMessages getValidationMessages() {
		return _validationMessages;
	}

	public void setValidationMessages(ValidationMessages messages) {
		_validationMessages = messages;
	}

	/**
	 * did activate when the session is activating
	 */
	private void didActivate() {
		_activating = true;
		try {
			_log.debug("didActivate : [{}]", BinderImpl.this);
			//re-tie value to tracker.
			loadComponent(_rootComp, false);
		} finally {
			_activating = false;
		}
	}

	/**
	 * object that store in root component to help activating.
	 */
	private class Activator implements ComponentActivationListener, Serializable {
		private static final long serialVersionUID = 1L;

		public void didActivate(Component comp) {
			if (_rootComp.equals(comp)) {
				//zk 1442, don't do multiple subscribed if didActivate is called every request (e.x. jboss5)
				initQueue();
				if (_deferredActivator == null) {
					//defer activation to execution only for the first didActivate when failover
					comp.getDesktop().addListener(_deferredActivator = new DeferredActivator());
				}
			}
		}

		public void willPassivate(Component comp) {
			//zk 1442, do nothing
		}
	}

	/**
	 * object that store in desktop listener to help activating.
	 * it do the activation when first execution come into
	 */
	private class DeferredActivator implements ExecutionInit, Serializable {
		private static final long serialVersionUID = 1L;

		public void init(Execution exec, Execution parent) throws Exception {
			Desktop desktop = exec.getDesktop();
			desktop.removeListener(_deferredActivator);
			BinderImpl.this.didActivate();
		}
	}

	public BindingExecutionInfoCollector getBindingExecutionInfoCollector() {
		DebuggerFactory factory = DebuggerFactory.getInstance();
		if (factory == null) return null;
		// ZK-5048: setViewModelClass for MVVM DebuggerFactory to log via SLF4J
		BindingExecutionInfoCollector collector = factory.getExecutionInfoCollector();
		if (collector instanceof DefaultExecutionInfoCollector) {
			Class vmClass = BindUtils.getViewModelClass(getViewModelInView());
			((DefaultExecutionInfoCollector) collector).setViewModelClass(vmClass);
		}
		return collector;
	}

	public BindingAnnotationInfoChecker getBindingAnnotationInfoChecker() {
		DebuggerFactory factory = DebuggerFactory.getInstance();
		if (factory == null) return null;
		// ZK-5048: setViewModelClass for MVVM DebuggerFactory to log via SLF4J
		BindingAnnotationInfoChecker checker = factory.getAnnotationInfoChecker();
		if (checker instanceof DefaultAnnotationInfoChecker) {
			Class vmClass = BindUtils.getViewModelClass(getViewModelInView());
			((DefaultAnnotationInfoChecker) checker).setViewModelClass(vmClass);
		}
		return checker;
	}

	public String getQueueName() {
		return _quename;
	}

	public String getQueueScope() {
		return _quescope;
	}

	public Map getMatchMediaValue() {
		if (_matchMediaValues == null) {
			_matchMediaValues = initMatchMediaValues(getViewModel());
		}
		return Collections.unmodifiableMap(_matchMediaValues);
	}

	private Map> initSaveFormMap() {
		if (_saveFormFields == null) {
			_saveFormFields = new HashMap>(4);
		}
		return _saveFormFields;
	}

	public void addSaveFormFieldName(Form form, String fieldName) {
		Set fields = initSaveFormMap().get(form);
		if (fields == null) {
			fields = new HashSet(16);
			_saveFormFields.put(form, fields);
		}
		fields.add(fieldName);
	}

	public void addSaveFormFieldName(Form form, Set fieldNames) {
		Set fields = initSaveFormMap().get(form);
		if (fields == null) {
			fields = new HashSet(16);
			_saveFormFields.put(form, fields);
		}
		fields.addAll(fieldNames);

	}

	@SuppressWarnings("unchecked")
	public Set removeSaveFormFieldNames(Form self) {
		Set result = initSaveFormMap().remove(self);
		if (result == null)
			return Collections.EMPTY_SET;
		return result;
	}

	@SuppressWarnings("unchecked")
	public Set getSaveFormFieldNames(Form form) {
		Set result = initSaveFormMap().get(form);
		if (result == null)
			return Collections.EMPTY_SET;
		return result;
	}

	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();
		init();
	}

	/**
	 * Internal use only.
	 * Check and init queue
	 */
	public void initQueue() {
		//subscribe queue
		if (!isSubscribed(_quename, _quescope, _queueListener))
			subscribeQueue(_quename, _quescope, _queueListener);
	}

	/**
	 * Internal use only.
	 * Check and init Activator
	 */
	public void initActivator() {
		if (_rootComp != null && !_rootComp.hasAttribute(ACTIVATOR))
			_rootComp.setAttribute(ACTIVATOR, new Activator()); //keep only one instance in root comp
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy