zk/src/main/java/org/zkoss/zk/ui/AbstractComponent.java

Summary

Maintainability
F
1 mo
Test Coverage
/* AbstractComponent.java

    Purpose:

    Description:

    History:
        Mon May 30 21:49:42     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui;

import static org.zkoss.lang.Generics.cast;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Method;
import java.util.AbstractSequentialList;
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.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

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

import org.zkoss.io.Serializables;
import org.zkoss.json.JavaScriptValue;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.util.CollectionsX;
import org.zkoss.util.Converter;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.au.AuService;
import org.zkoss.zk.au.out.AuClientInfo;
import org.zkoss.zk.au.out.AuEcho;
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.Events;
import org.zkoss.zk.ui.event.ForwardEvent;
import org.zkoss.zk.ui.ext.Macro;
import org.zkoss.zk.ui.ext.NonFellow;
import org.zkoss.zk.ui.ext.RawId;
import org.zkoss.zk.ui.ext.Scope;
import org.zkoss.zk.ui.ext.ScopeListener;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.impl.SimpleScope;
import org.zkoss.zk.ui.impl.Utils;
import org.zkoss.zk.ui.metainfo.Annotation;
import org.zkoss.zk.ui.metainfo.AnnotationMap;
import org.zkoss.zk.ui.metainfo.ComponentDefinition;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.metainfo.DefinitionNotFoundException;
import org.zkoss.zk.ui.metainfo.EventHandler;
import org.zkoss.zk.ui.metainfo.EventHandlerMap;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.metainfo.PageDefinition;
import org.zkoss.zk.ui.metainfo.ShadowInfo;
import org.zkoss.zk.ui.metainfo.ZScript;
import org.zkoss.zk.ui.select.Selectors;
import org.zkoss.zk.ui.sys.Attributes;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.ComponentRedraws;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.ContentRenderer;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.sys.EventListenerMap;
import org.zkoss.zk.ui.sys.ExecutionCtrl;
import org.zkoss.zk.ui.sys.HtmlPageRenders;
import org.zkoss.zk.ui.sys.IdGenerator;
import org.zkoss.zk.ui.sys.JSCumulativeContentRenderer;
import org.zkoss.zk.ui.sys.JsContentRenderer;
import org.zkoss.zk.ui.sys.Names;
import org.zkoss.zk.ui.sys.PropertiesRenderer;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.ShadowElementsCtrl;
import org.zkoss.zk.ui.sys.StringPropertyAccess;
import org.zkoss.zk.ui.sys.StubComponent;
import org.zkoss.zk.ui.sys.StubsComponent;
import org.zkoss.zk.ui.sys.UiEngine;
import org.zkoss.zk.ui.sys.WebAppCtrl;
import org.zkoss.zk.ui.util.Callback;
import org.zkoss.zk.ui.util.ComponentActivationListener;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zk.ui.util.ComponentSerializationListener;
import org.zkoss.zk.ui.util.Template;

/**
 * A skeletal implementation of {@link Component}.
 *
 * @author tomyeh
 */
public class AbstractComponent implements Component, ComponentCtrl, java.io.Serializable {
    private static final Logger log = LoggerFactory.getLogger(AbstractComponent.class);
    private static final long serialVersionUID = 20100719L;

    /** Map(Class, Map(String name, Integer flags)). */
    private static final Map<Class<? extends Component>, Map<String, Integer>> _clientEvents = new ConcurrentHashMap<Class<? extends Component>, Map<String, Integer>>(
            128);
    private static final String DEFAULT = Impls.DEFAULT;
    private static final String ANONYMOUS_ID = "z__i";
    /** Used to generate an anonymous ID. */
    private static int _anonymousId;
    /** Library property key. */
    private static final String AUTO_REMOVE_NULL = "org.zkoss.zk.ui.component.autoRemoveNullAttribute.enabled";

    /*package*/ transient Page _page;
    private String _id = "";
    private String _uuid;
    private transient ComponentDefinition _def;

    /*package*/ transient AbstractComponent _parent; //called by HtmlShadowElement
    /** The next sibling. */
    /*package*/ transient AbstractComponent _next;
    /** The previous sibling. */
    /*package*/ transient AbstractComponent _prev;
    /** ChildInfo: use a class (rather than multiple member) to save footprint */
    /*package*/ transient ChildInfo _chdinf;
    /** AuxInfo: use a class (rather than multiple member) to save footprint */
    private AuxInfo _auxinf;

    private Map<String, ShadowElement> _shadowIdMap; //to speed up id only shadow selector
    /** Whether this component is initialized. */
    private boolean _initialized;

    /** Constructs a component with auto-generated ID.
     * @since 3.0.7 (becomes public)
     */
    public AbstractComponent() {
        final String mold = getDefaultMold(getClass());
        if (mold != null && mold.length() > 0 && !DEFAULT.equals(mold))
            initAuxInfo().mold = mold;

        final Execution exec = Executions.getCurrent();
        final Object curInfo = ComponentsCtrl.getCurrentInfo();
        if (curInfo != null) {
            ComponentsCtrl.setCurrentInfo((ComponentInfo) null); //to avoid mis-use
            if (curInfo instanceof ComponentInfo) {
                final ComponentInfo compInfo = (ComponentInfo) curInfo;
                _def = compInfo.getComponentDefinition();
                addSharedAnnotationMap(_def.getAnnotationMap());
                addSharedAnnotationMap(compInfo.getAnnotationMap());
                //F80 - store subtree's binder annotation count
                if (compInfo.hasBindingAnnotation())
                    initBindingAnnotation();
            } else if (curInfo instanceof ShadowInfo) {
                final ShadowInfo compInfo = (ShadowInfo) curInfo;
                _def = compInfo.getComponentDefinition();
                addSharedAnnotationMap(_def.getAnnotationMap());
                addSharedAnnotationMap(compInfo.getAnnotationMap());
                //F80 - store subtree's binder annotation count
                if (compInfo.hasBindingAnnotation())
                    initBindingAnnotation();
            } else {
                _def = (ComponentDefinition) curInfo;
                addSharedAnnotationMap(_def.getAnnotationMap());
            }
        } else {
            _def = Impls.getDefinition(exec, getClass());
            if (_def != null)
                addSharedAnnotationMap(_def.getAnnotationMap());
            else
                _def = ComponentsCtrl.DUMMY;
        }

        addSharedAnnotationMap(Impls.getClassAnnotationMap(this.getClass()));

        if (this instanceof IdSpace)
            initAuxInfo().spaceInfo = new SpaceInfo();

        _def.applyAttributes(this);
        //        if (log.isDebugEnabled()) log.debug("Create comp: "+this);
    }

    /** Constructs a dummy component that is not associated
     * with any component definition.
     * @param useless an useless argument (it is ignored but used
     * to distinguish the default constructor)
     * @since 6.0.0
     */
    protected AbstractComponent(boolean useless) {
        _def = ComponentsCtrl.DUMMY;
    }

    /** The default implementation for {@link #getChildren}.
     * It is suggested to extend this class if you want to override
     * {@link #getChildren} to instantiate your own instance.
     * @since 3.5.1
     */
    protected class Children extends AbstractSequentialList<Component> {
        public int size() {
            return nChild();
        }

        public ListIterator<Component> listIterator(int index) {
            return new ChildIter(index);
        }
    }

    /** Adds to the ID spaces, if any, when ID is changed.
     * Caller has to make sure the uniqueness (and not auto id).
     */
    private static void addToIdSpaces(final Component comp) {
        final String compId = comp.getId();
        if (comp instanceof NonFellow || isAutoId(compId))
            return; //nothing to do

        if (comp instanceof IdSpace)
            ((AbstractComponent) comp).bindToIdSpace(comp);

        addFellow(comp, getSpaceOwnerOfParent(comp));
    }

    private static void addFellow(Component comp, IdSpace owner) {
        if (owner instanceof Component)
            ((AbstractComponent) owner).bindToIdSpace(comp);
        else if (owner instanceof Page)
            ((AbstractPage) owner).addFellow(comp);
        if (owner == null && comp instanceof ShadowElement)
            addToShadowIdMap(comp);
    }

    private static void removeFellow(Component comp, IdSpace owner) {
        if (owner instanceof Component)
            ((AbstractComponent) owner).unbindFromIdSpace(comp.getId());
        else if (owner instanceof Page)
            ((AbstractPage) owner).removeFellow(comp);
        if (owner == null && comp instanceof ShadowElement)
            removeFromShadowIdMap(comp);
    }

    private static IdSpace getSpaceOwnerOfParent(Component comp) {
        final Component parent = comp.getParent();
        return parent != null ? spaceOwnerNoVirtual(parent)
                : //ignore virtual IdSpace
                comp.getPage();
    }

    /** Removes from the ID spaces, if any, when ID is changed. */
    private static void removeFromIdSpaces(final Component comp) {
        final String compId = comp.getId();
        if (comp instanceof NonFellow || isAutoId(compId))
            return; //nothing to do

        if (comp instanceof IdSpace)
            ((AbstractComponent) comp).unbindFromIdSpace(compId);

        removeFellow(comp, getSpaceOwnerOfParent(comp));
    }

    /** Checks the uniqueness in ID space when changing ID. */
    private static void checkIdSpaces(final AbstractComponent comp, String newId) {
        if (comp instanceof NonFellow)
            return; //no need to check

        if (comp instanceof IdSpace && comp._auxinf.spaceInfo.fellows.containsKey(newId))
            throw new UiException("Not unique in the ID space of " + comp);

        final IdSpace is = getSpaceOwnerOfParent(comp);
        if (is instanceof Component) {
            if (((AbstractComponent) is)._auxinf.spaceInfo.fellows.containsKey(newId))
                throw new UiException("Not unique in ID space " + is + ": " + newId);
        } else if (is != null) {
            if (is.hasFellow(newId))
                throw new UiException("Not unique in ID space " + is + ": " + newId);
        }
    }

    /*package*/ static boolean isAutoId(String compId) {
        return compId.length() == 0;
    }

    /** Adds its descendants to the ID space when parent or page is changed,
     * excluding comp.
     */
    private static void addToIdSpacesDown(Component comp) {
        addToIdSpacesDown(comp, getSpaceOwnerOfParent(comp));
    }

    private static void addToIdSpacesDown(Component comp, IdSpace owner) {
        if (!(comp instanceof NonFellow) && !isAutoId(comp.getId()))
            addFellow(comp, owner);

        if (!(comp instanceof IdSpace))
            for (AbstractComponent ac = (AbstractComponent) comp.getFirstChild(); ac != null; ac = ac._next)
                addToIdSpacesDown(ac, owner); //recursive

        ((AbstractComponent) comp).notifyIdSpaceChanged(owner);
    }

    private void notifyIdSpaceChanged(IdSpace newIdSpace) {
        if (_auxinf != null && _auxinf.attrs != null)
            _auxinf.attrs.notifyIdSpaceChanged(newIdSpace);
    }

    /** Adds its descendants to the ID space when parent or page is changed,
     * excluding comp.
     */
    private static void removeFromIdSpacesDown(Component comp) {
        removeFromIdSpacesDown(comp, getSpaceOwnerOfParent(comp));
    }

    private static void removeFromIdSpacesDown(Component comp, IdSpace owner) {
        if (!(comp instanceof NonFellow) && !isAutoId(comp.getId()))
            removeFellow(comp, owner);

        if (!(comp instanceof IdSpace))
            for (AbstractComponent ac = (AbstractComponent) comp.getFirstChild(); ac != null; ac = ac._next)
                removeFromIdSpacesDown(ac, owner); //recursive

        ((AbstractComponent) comp).notifyIdSpaceChanged(null);
    }

    /** Checks the uniqueness in ID space when changing parent. */
    private static void checkIdSpacesDown(Component comp, Component newparent) {
        final IdSpace is = spaceOwnerNoVirtual(newparent); //exclude virtual IdSpace
        //for checking, it is better NOT to ignore virtual IdSpace
        //but, for better performance, we don't.
        if (is != null)
            checkIdSpacesDown(comp, is);
    }

    private static void checkIdSpacesDown(Component comp, IdSpace owner) {
        final String compId = comp.getId();
        if (!(comp instanceof NonFellow) && !isAutoId(compId)
                && (owner instanceof Component
                        ? ((AbstractComponent) owner)._auxinf.spaceInfo.fellows.containsKey(compId)
                        : owner.hasFellow(compId)))
            throw new UiException("Not unique in the ID space of " + owner + ": " + compId);
        if (!(comp instanceof IdSpace))
            for (AbstractComponent ac = (AbstractComponent) comp.getFirstChild(); ac != null; ac = ac._next)
                checkIdSpacesDown(ac, owner); //recursive
    }

    /** Bind comp to this ID space (owned by this component).
     * Called only if IdSpace is implemented.
     * comp's ID must be unique (and not auto id)
     */
    private void bindToIdSpace(Component comp) {
        _auxinf.spaceInfo.fellows.put(comp.getId(), comp);
    }

    /** Unbind comp from this ID space (owned by this component).
     * Called only if IdSpace is implemented.
     */
    private void unbindFromIdSpace(String compId) {
        _auxinf.spaceInfo.fellows.remove(compId);
    }

    //-- Extra utilities --//
    /** Returns the UI engine based on {@link #_page}'s getDesktop().
     * Don't call this method when _page is null.
     */
    private UiEngine getAttachedUiEngine() {
        return ((WebAppCtrl) _page.getDesktop().getWebApp()).getUiEngine();
    }

    /** Returns the UI engine of the current execution, or null
     * if no current execution.
     */
    private UiEngine getCurrentUiEngine() {
        final Execution exec = Executions.getCurrent();
        return exec != null ? ((WebAppCtrl) exec.getDesktop().getWebApp()).getUiEngine() : null;
    }

    //-- Component --//
    public Page getPage() {
        return _page;
    }

    public Desktop getDesktop() {
        return _page != null ? _page.getDesktop() : null;
    }

    public void setPage(Page page) {
        if (page != _page)
            setPageBefore(page, null); //append
    }

    public void setPageBefore(Page page, Component refRoot) {
        beforeComponentPageChanged(page);
        if (refRoot != null && (page == null || refRoot.getParent() != null || refRoot.getPage() != page))
            refRoot = null;
        if (refRoot != null /*&& refRoot.getPage() == page (checked)*/
                && (refRoot == this || refRoot == _next))
            return; //nothing to do

        if (_parent != null)
            throw new UiException("Only the parent of a root component can be changed: " + this);

        final Page oldpg = _page;
        final boolean samepg = page == _page;
        if (!samepg) {
            if (page != null) {
                if (_page == null)
                    clearVirtualIdSpace(); //clear if being attached
                else if (_page.getDesktop() != page.getDesktop())
                    throw new UiException("The new page must be in the same desktop: " + page);
                //Not allow developers to access two desktops simultaneously
                checkIdSpacesDown(this, page);

                //No need to check UUID since checkIdSpacesDown covers it
                //-- a page is an ID space
            } else { //detach from a page
                checkDetach(_page);
            }

            if (_page != null)
                removeFromIdSpacesDown(this);
        }

        addMoved(_parent, _page, page); //Not depends on UUID
        if (!samepg)
            setPage0(page); //UUID might be changed here
        if (page != null && (samepg || refRoot != null))
            ((AbstractPage) page).moveRoot(this, refRoot);

        if (!samepg && _page != null)
            addToIdSpacesDown(this);

        afterComponentPageChanged(page, oldpg);
    }

    /** Checks whether it is OK to detach the specified page.
     * @param page the page to detach (never null).
     */
    private static void checkDetach(Page page) {
        final Execution exec = Executions.getCurrent();
        if (exec == null)
            throw new UiException("You cannot access a desktop other than an event listener");
        if (page.getDesktop() != exec.getDesktop())
            throw new UiException("You cannot access components belong to other desktop");
    }

    /** Called when this component is moved from the specified parent
     * and/or page to the new page.
     *
     * <p>Default: it notifies {@link UiEngine} to update the component
     * at the client (usually remove-and-add).
     *
     * <p>It is designed to let derived classes overriding this method
     * to disable this update. However, you rarely need to override it.
     * One possible but rare case: the component's
     * visual part at the client updates the visual representation
     * at the client and then notify the component at the server
     * to update its children accordingly. In this case, it is redundant
     * if we ask UI Engine to send the updates to client.
     *
     * @param oldparent the parent before moved.
     * The new parent can be found by calling {@link #getParent}.
     * @param oldpg the parent before moved.
     * @param newpg the new page. {@link #getPage} might return
     * the old page.
     */
    protected void addMoved(Component oldparent, Page oldpg, Page newpg) {
        final Desktop dt;
        if (oldpg != null)
            dt = oldpg.getDesktop();
        else if (newpg != null)
            dt = newpg.getDesktop();
        else
            return;
        ((WebAppCtrl) dt.getWebApp()).getUiEngine().addMoved(this, oldparent, oldpg, newpg);
    }

    /** Set the page without fixing IdSpace
     */
    private void setPage0(Page page) {
        if (page == _page)
            return; //nothing changed

        //assert _parent == null || _parent.getPage() == page;
        //detach
        final boolean bRoot = _parent == null;
        if (_page != null) {
            if (bRoot)
                ((AbstractPage) _page).removeRoot(this);
            if (page == null && ((DesktopCtrl) _page.getDesktop()).removeComponent(this)
                    && !(this instanceof StubComponent)) { //Bug ZK-1452: don't need to reset StubComponent's uuid
                //    resetUuid = true; //recycled (so reset it -- refer to DesktopImpl for reason)
            }
        }

        final Page oldpage = _page;
        _page = page;

        if (_page != null) {
            if (bRoot)
                ((AbstractPage) _page).addRoot(this); //Not depends on uuid
            final Desktop desktop = _page.getDesktop();
            if (oldpage == null) {
                if (_uuid == null || _uuid.startsWith(ANONYMOUS_ID) || desktop.getComponentByUuidIfAny(_uuid) != null)
                    _uuid = nextUuid(desktop);

                ((DesktopCtrl) desktop).addComponent(this); //depends on uuid
            }

            onPageAttached(_page, oldpage);
        } else {
            onPageDetached(oldpage);
        }

        //process all children recursively
        for (AbstractComponent p = (AbstractComponent) getFirstChild(); p != null; p = p._next)
            p.setPage0(page); //recursive
    }

    private String nextUuid(Desktop desktop) {
        for (int count = 0;;) {
            String uuid = ((DesktopCtrl) desktop).getNextUuid(this);
            if (desktop.getComponentByUuidIfAny(uuid) == null)
                return uuid;

            if (++count > 10000)
                throw new UiException(
                        "It took too much time to look for unique UUID. Please check the implementation of IdGenerator.");
        }
    }

    public String getId() {
        return _id;
    }

    public void setId(String id) {
        if (id == null)
            id = "";
        if (!id.equals(_id)) {
            final boolean rawId = this instanceof RawId;
            String newUuid = null;
            if (rawId)
                newUuid = id;

            if (!id.isEmpty()) {
                if (Names.isReserved(id))
                    throw new UiException(
                            "Invalid ID: " + id + ". Cause: reserved words not allowed: " + Names.getReservedNames());

                if (rawId && _page != null) {
                    final Component c = _page.getDesktop().getComponentByUuidIfAny(newUuid);
                    if (c != null && c != this)
                        throw new UiException("Replicated UUID is not allowed for " + getClass() + ": " + newUuid);
                }

                checkIdSpaces(this, id);
            }

            removeFromIdSpaces(this);
            if (rawId) { //we have to change UUID
                if (_page != null) {
                    //called before uuid is changed
                    final Desktop dt = _page.getDesktop();
                    ((DesktopCtrl) dt).removeComponent(this);

                    if (newUuid == null || newUuid.isEmpty())
                        newUuid = nextUuid(dt);

                    if (!Objects.equals(_uuid, newUuid))
                        getAttachedUiEngine().addUuidChanged(this);
                }

                _id = id;
                _uuid = newUuid;

                if (_page != null) {
                    ((DesktopCtrl) _page.getDesktop()).addComponent(this);
                }
            } else {
                _id = id;
            }

            addToIdSpaces(this);

            smartUpdate("id", _id);
        }
    }

    public String getUuid() {
        if (_uuid == null) {
            Execution exec = null; // ZK-2606: if parent has _page, child is in the same component tree
            for (Component comp = this; comp != null; comp = comp.getParent()) {
                if (comp.getPage() != null) {
                    exec = Executions.getCurrent();
                    break;
                }
            }
            if (exec == null) {
                IdGenerator idGenerator = ((WebAppCtrl) WebApps.getCurrent()).getIdGenerator();
                final String uid = idGenerator != null ? idGenerator.nextAnonymousComponentUuid(this, Utils.getComponentInfo(this)) : null;
                _uuid = ANONYMOUS_ID + (uid != null ? uid : String.valueOf(_anonymousId++));
            } else {
                _uuid = nextUuid(exec.getDesktop());
            }
            //OK to race for _anonymousId (since ok to be the same)
        }
        return _uuid;
    }

    public IdSpace getSpaceOwner() {
        return spaceOwner(this, false);
    }

    private static IdSpace spaceOwnerNoVirtual(Component p) {
        return spaceOwner(p, true);
    }

    private static IdSpace spaceOwner(Component p, boolean ignoreVirtualIS) {
        Component top;
        do {
            if (p instanceof IdSpace)
                return (IdSpace) p;
            top = p;
        } while ((p = p.getParent()) != null);

        final AbstractComponent ac = (AbstractComponent) top;
        return ac._page != null ? ac._page : ignoreVirtualIS ? null : ac.getVirtualIdSpace();
    }

    /** Returns the UI object that will serve as a space owner.
     * Unlike {@link #spaceOwner}, it will return the top component if
     * it is a virtual IdSpace. Furthermore, clearVirtualIdSpace will be called
     * before returned. It is used only by {@link #setParent}.
     */
    private static Object spaceController(Component p) {
        Component top;
        do {
            if (p instanceof IdSpace)
                return p;
            top = p;
        } while ((p = p.getParent()) != null);

        final AbstractComponent ac = (AbstractComponent) top;
        if (ac._page != null)
            return ac._page;
        ac.clearVirtualIdSpace();
        return ac; //yes, return the top (virtual ID space)
    }

    /** Called only if this is root and has no page. The caller has to make sure it.
     */
    private IdSpace getVirtualIdSpace() {
        if (_chdinf != null) {
            if (_chdinf.vispace == null)
                _chdinf.vispace = new VirtualIdSpace(this);
            return _chdinf.vispace;
        }
        return new VirtualIdSpace(this);
        //no need to cache since it is fast and small (since no child)
    }

    /** It must be called if a root component without page and doesn't implement
     * idspace: 1) is added to a page, 2) is added to a component,
     * 3) add or remove a new descendant (not under another IdSpace).
     * It is harmless if called redundantly.
     */
    private void clearVirtualIdSpace() {
        if (_chdinf != null)
            _chdinf.vispace = null;
    }

    public boolean hasFellow(String compId) {
        if (this instanceof IdSpace)
            return _auxinf.spaceInfo.fellows.containsKey(compId);

        final IdSpace idspace = getSpaceOwner();
        return idspace != null && idspace.hasFellow(compId);
    }

    public boolean hasFellow(String compId, boolean recurse) {
        return getFellowIfAny(compId, recurse) != null;
    }

    public Component getFellow(String compId) throws ComponentNotFoundException {
        if (this instanceof IdSpace) {
            final Component comp = _auxinf.spaceInfo.fellows.get(compId);
            if (comp == null)
                throw new ComponentNotFoundException("Fellow component not found: " + compId);
            return comp;
        }

        final IdSpace idspace = getSpaceOwner();
        if (idspace == null)
            throw new ComponentNotFoundException("This component doesn't belong to any ID space: " + this);
        return idspace.getFellow(compId);
    }

    public Component getFellow(String compId, boolean recurse) throws ComponentNotFoundException {
        final Component comp = getFellowIfAny(compId, recurse);
        if (comp == null)
            throw new ComponentNotFoundException("Fellow component not found: " + compId);
        return comp;
    }

    public Component getFellowIfAny(String compId) {
        if (this instanceof IdSpace)
            return _auxinf.spaceInfo.fellows.get(compId);

        final IdSpace idspace = getSpaceOwner();
        return idspace == null ? null : idspace.getFellowIfAny(compId);
    }

    public Component getFellowIfAny(String compId, boolean recurse) {
        if (!recurse)
            return getFellowIfAny(compId);

        for (IdSpace idspace = getSpaceOwner(); idspace != null;) {
            Component f = idspace.getFellowIfAny(compId);
            if (f != null)
                return f;
            idspace = Components.getParentIdSpace(idspace);
        }
        return null;
    }

    public Collection<Component> getFellows() {
        if (this instanceof IdSpace)
            return Collections.unmodifiableCollection(_auxinf.spaceInfo.fellows.values());

        final IdSpace idspace = getSpaceOwner();
        if (idspace != null)
            return idspace.getFellows();
        return Collections.emptyList();
    }

    public Component getNextSibling() {
        return _next;
    }

    public Component getPreviousSibling() {
        return _prev;
    }

    public Component getFirstChild() {
        return _chdinf != null ? _chdinf.first : null;
    }

    public Component getLastChild() {
        return _chdinf != null ? _chdinf.last : null;
    }

    /** Returns the number of children. */
    @SuppressWarnings("checkstyle:MethodName")
    /*package*/ final int nChild() { //called by HtmlNativeComponent
        return _chdinf != null ? _chdinf.nChild : 0;
    }

    /** Returns the number of children. It assumes _chdinf not null. */
    @SuppressWarnings("checkstyle:MethodName")
    /*package*/ final void nChild(AbstractComponent first, AbstractComponent last, int nChild) {
        _chdinf.first = first;
        _chdinf.last = last;
        _chdinf.nChild = nChild;

        for (; first != null; first = first._next)
            first._parent = this;
    }

    /*package*/ int modCntChd() { // called by HtmlShadowElement
        return _chdinf != null ? _chdinf.modCntChd : 0;
    }

    public String setWidgetListener(String evtnm, String script) {
        if (evtnm == null)
            throw new IllegalArgumentException();

        final String old;
        if (script != null) {
            if (initAuxInfo().wgtlsns == null)
                _auxinf.wgtlsns = new LinkedHashMap<String, String>(4);
            old = _auxinf.wgtlsns.put(evtnm, script);
        } else
            old = _auxinf != null && _auxinf.wgtlsns != null ? _auxinf.wgtlsns.remove(evtnm) : null;
        if (!Objects.equals(script, old))
            smartUpdateWidgetListener(evtnm, script);
        return old;
    }

    public String getWidgetListener(String evtnm) {
        return _auxinf != null && _auxinf.wgtlsns != null ? _auxinf.wgtlsns.get(evtnm) : null;
    }

    public Set<String> getWidgetListenerNames() {
        if (_auxinf != null && _auxinf.wgtlsns != null)
            return _auxinf.wgtlsns.keySet();
        return Collections.emptySet();
    }

    public String setWidgetOverride(String name, String script) {
        if (name == null)
            throw new IllegalArgumentException();

        final String old;
        if (script != null) {
            if (initAuxInfo().wgtovds == null)
                _auxinf.wgtovds = new LinkedHashMap<String, String>(4);
            old = _auxinf.wgtovds.put(name, script);
        } else
            old = _auxinf != null && _auxinf.wgtovds != null ? _auxinf.wgtovds.remove(name) : null;
        if (!Objects.equals(script, old))
            smartUpdateWidgetOverride(name, script);
        return old;
    }

    public String getWidgetOverride(String name) {
        return _auxinf != null && _auxinf.wgtovds != null ? _auxinf.wgtovds.get(name) : null;
    }

    public Set<String> getWidgetOverrideNames() {
        if (_auxinf != null && _auxinf.wgtovds != null)
            return _auxinf.wgtovds.keySet();
        return Collections.emptySet();
    }

    public String setClientAttribute(String name, String value) {
        if (name == null)
            throw new IllegalArgumentException();

        final String old;
        if (value != null) {
            if (initAuxInfo().domattrs == null)
                _auxinf.domattrs = new LinkedHashMap<String, String>(4);
            old = _auxinf.domattrs.put(name, value);
        } else
            old = _auxinf != null && _auxinf.domattrs != null ? _auxinf.domattrs.remove(name) : null;
        return old;
    }

    public String getClientAttribute(String name) {
        return _auxinf != null && _auxinf.domattrs != null ? _auxinf.domattrs.get(name) : null;
    }

    public Set<String> getWidgetAttributeNames() {
        if (_auxinf != null && _auxinf.domattrs != null)
            return _auxinf.domattrs.keySet();
        return Collections.emptySet();
    }

    public String setClientDataAttribute(String name, String value) {
        String old = setClientAttribute("data-" + name, value);
        if (old != null)
            invalidate();
        return old;
    }

    public String getClientDataAttribute(String name) {
        return getClientAttribute("data-" + name);
    }

    public Map<String, Object> getAttributes(int scope) {
        switch (scope) {
        case SPACE_SCOPE:
            if (this instanceof IdSpace)
                return getAttributes();
            final IdSpace idspace = getSpaceOwner();
            if (idspace != null)
                return idspace.getAttributes();
            return Collections.emptyMap();
        case PAGE_SCOPE:
            if (_page != null)
                return _page.getAttributes();
            return Collections.emptyMap();
        case DESKTOP_SCOPE:
            if (_page != null)
                return _page.getDesktop().getAttributes();
            return Collections.emptyMap();
        case SESSION_SCOPE:
            if (_page != null)
                return _page.getDesktop().getSession().getAttributes();
            return Collections.emptyMap();
        case APPLICATION_SCOPE:
            if (_page != null)
                return _page.getDesktop().getWebApp().getAttributes();
            return Collections.emptyMap();
        case COMPONENT_SCOPE:
            return getAttributes();
        case REQUEST_SCOPE:
            final Execution exec = getExecution();
            if (exec != null)
                return exec.getAttributes();
            //fall thru
        default:
            return Collections.emptyMap();
        }
    }

    public Map<String, Object> getAttributes() {
        return attrs().getAttributes();
    }

    private SimpleScope attrs() {
        if (initAuxInfo().attrs == null)
            _auxinf.attrs = new SimpleScope(this);
        return _auxinf.attrs;
    }

    private Execution getExecution() {
        return _page != null ? _page.getDesktop().getExecution() : Executions.getCurrent();
    }

    public Object getAttribute(String name, int scope) {
        return getAttributes(scope).get(name);
    }

    public Object getAttribute(String name) {
        return _auxinf != null && _auxinf.attrs != null ? _auxinf.attrs.getAttribute(name) : null;
    }

    public Object getAttribute(String name, boolean recurse) {
        Object val = getAttribute(name);
        if (val != null || !recurse || hasAttribute(name))
            return val;

        if (_parent != null)
            return _parent.getAttribute(name, true);
        if (_page != null)
            return _page.getAttribute(name, true);
        return null;
    }

    public boolean hasAttribute(String name, int scope) {
        return getAttributes(scope).containsKey(name);
    }

    public boolean hasAttribute(String name) {
        return _auxinf != null && _auxinf.attrs != null && _auxinf.attrs.hasAttribute(name);
    }

    public boolean hasAttribute(String name, boolean recurse) {
        if (hasAttribute(name))
            return true;

        if (recurse) {
            if (_parent != null)
                return _parent.hasAttribute(name, true);
            if (_page != null)
                return _page.hasAttribute(name, true);
        }
        return false;
    }

    // Bug ZK-2789: allow null attribute values. If auto remove lib prop is
    // enabled, then set null attribute value = remove attribute
    public Object setAttribute(String name, Object value, int scope) {
        if (value == null && Boolean.parseBoolean(Library.getProperty(AUTO_REMOVE_NULL))) {
            // null value + old method = remove attribute
            return removeAttribute(name, scope);
        }
        // either 1. null value + new method or 2. non null value + old/new method
        final Map<String, Object> attrs = getAttributes(scope);
        if (attrs == Collections.EMPTY_MAP)
            throw new IllegalStateException("This component, " + this + ", doesn't belong to the "
                    + Components.scopeToString(scope) + " scope");
        return attrs.put(name, value);
    }

    // Bug ZK-2789: allow null attribute values. If auto remove lib prop is enabled,
    // then set null attribute value = remove attribute
    public Object setAttribute(String name, Object value) {
        if (value == null && shallAutoRemove()) {
            // null value + old method = remove attribute
            return removeAttribute(name);
        }
        // either 1. null value + new method or 2. non null value + old/new method
        return attrs().setAttribute(name, value);
    }

    public Object setAttribute(String name, Object value, boolean recurse) {
        if (recurse && !hasAttribute(name)) {
            if (_parent != null) {
                if (_parent.hasAttribute(name, true))
                    return _parent.setAttribute(name, value, true);
            } else if (_page != null) {
                if (_page.hasAttribute(name, true))
                    return _page.setAttribute(name, value, true);
            }
        }
        return setAttribute(name, value);
    }

    public Object removeAttribute(String name, int scope) {
        final Map attrs = getAttributes(scope);
        if (attrs == Collections.emptyMap())
            throw new IllegalStateException("This component doesn't belong to any ID space: " + this);
        return attrs.remove(name);
    }

    public Object removeAttribute(String name) {
        return _auxinf != null && _auxinf.attrs != null ? _auxinf.attrs.removeAttribute(name) : null;
    }

    public Object removeAttribute(String name, boolean recurse) {
        if (recurse && !hasAttribute(name)) {
            if (_parent != null) {
                if (_parent.hasAttribute(name, true))
                    return _parent.removeAttribute(name, true);
            } else if (_page != null) {
                if (_page.hasAttribute(name, true))
                    return _page.removeAttribute(name, true);
            }
            return null;
        }
        return removeAttribute(name);
    }

    private Boolean hasInited = null;

    private boolean shallAutoRemove() {
        if (hasInited == null) {
            hasInited = Boolean.parseBoolean(Library.getProperty(AUTO_REMOVE_NULL));
        }
        return hasInited.booleanValue();
    }

    public Object getAttributeOrFellow(String name, boolean recurse) {
        Object val = getAttribute(name);
        if (val != null || hasAttribute(name))
            return val;

        if (this instanceof IdSpace) { //fellow last
            val = getFellowIfAny(name);
            if (val != null)
                return val;
        }

        if (recurse) {
            if (_parent != null)
                return _parent.getAttributeOrFellow(name, true);
            if (_page != null)
                return _page.getAttributeOrFellow(name, true);
            if (this instanceof ShadowElement) {
                Component shadowHost = ((ShadowElement) this).getShadowHost();
                if (shadowHost != null)
                    return shadowHost.getAttributeOrFellow(name, true);
            }
            if (!(this instanceof IdSpace))
                return getVirtualIdSpace().getFellowIfAny(name);
        }
        return null;
    }

    private boolean _variableSeeking = false;

    public Object getShadowVariable(String name, boolean recurse) {
        return getShadowVariable(this, name, recurse);
    }

    public Object getShadowVariable(Component baseChild, String name, boolean recurse) {
        try {
            _variableSeeking = true;
            return getShadowVariable0(baseChild, name, recurse);
        } finally {
            _variableSeeking = false;
        }
    }

    protected Object getShadowVariable0(Component baseChild, String name, boolean recurse) {
        try {
            _variableSeeking = true;
            Object val = getAttribute(name);
            if (val != null || hasAttribute(name))
                return val;

            if (this instanceof ShadowElement) {
                // Bug fixed for the test case SimpleELResolverTest.java
                final Object value = ((ShadowElementCtrl) this).resolveVariable(null, name, recurse);
                if (value != null)
                    return value;
            } else {
                ComponentCtrl ctrl = this;
                List<HtmlShadowElement> shadowRoots = ctrl.getShadowRoots();
                if (!shadowRoots.isEmpty()) {
                    Map<Component, Integer> indexCacheMap = getIndexCacheMap(this);
                    try {
                        if (indexCacheMap != null) {
                            Iterator<Component> iterator = indexCacheMap.keySet()
                                    .iterator();
                            Component parent = null;
                            if (iterator.hasNext() && (parent = iterator.next().getParent()) != baseChild
                                    && parent != baseChild.getParent()) {
                                // check whether it's the same parent
                                destroyIndexCacheMap(this); // reset
                            }
                        }
                        initIndexCacheMap(this);

                        for (HtmlShadowElement shadow : shadowRoots) {
                            if (shadow.getShadowHost() != baseChild) {
                                switch (HtmlShadowElement.inRange(shadow, baseChild)) {
                                case IN_RANGE:
                                case FIRST:
                                case LAST:
                                    HtmlShadowElement current = findNearestShadow(shadow, baseChild);
                                    return current.resolveVariable(baseChild, name, recurse);
                                }
                            } else { // matched, try args
                                Map<?, ?> execArgs = getExecution().getArg();
                                if (execArgs.containsKey(name)) {
                                    return execArgs.get(name);
                                }
                            }
                        }
                    } finally {
                        ShadowElementsCtrl.setDistributedIndexInfo(this, indexCacheMap);
                    }
                }
            }
            if (recurse) {
                if (_parent != null)
                    return _parent.getShadowVariable0(this, name, recurse);

                if (this instanceof ShadowElement) {
                    AbstractComponent shadowHost = (AbstractComponent) ((ShadowElement) this).getShadowHost();
                    if (shadowHost != null) {
                        if (shadowHost._variableSeeking) {
                            if (shadowHost.getParent() != null) {
                                return ((AbstractComponent) shadowHost.getParent()).getShadowVariable0(shadowHost, name,
                                        recurse);
                            }
                            return null; // avoid deadloop
                        }
                        return ((AbstractComponent) shadowHost).getShadowVariable0(shadowHost, name, recurse);
                    }
                }
            }
            return null;
        } finally {
            _variableSeeking = false;
        }
    }

    private HtmlShadowElement findNearestShadow(HtmlShadowElement current, Component baseChild) {
        List<ShadowElement> list = cast(current.getChildren());
        for (ShadowElement sh : list) {
            if (sh instanceof HtmlShadowElement) {
                HtmlShadowElement shadow0 = (HtmlShadowElement) sh;
                switch (HtmlShadowElement.inRange(shadow0, baseChild)) {
                    case IN_RANGE:
                    case FIRST:
                    case LAST:
                        return findNearestShadow(shadow0, baseChild);
                }
            }
        }
        return current;
    }

    public boolean hasAttributeOrFellow(String name, boolean recurse) {
        if (hasAttribute(name) || (this instanceof IdSpace && hasFellow(name)))
            return true;

        if (recurse) {
            if (_parent != null)
                return _parent.hasAttributeOrFellow(name, true);
            if (_page != null)
                return _page.hasAttributeOrFellow(name, true);
            if (this instanceof ShadowElement) {
                Component shadowHost = ((ShadowElement) this).getShadowHost();
                if (shadowHost != null)
                    return shadowHost.hasAttributeOrFellow(name, true);
            }
            if (!(this instanceof IdSpace))
                return getVirtualIdSpace().hasFellow(name);
        }
        return false;
    }

    public boolean addScopeListener(ScopeListener listener) {
        return attrs().addScopeListener(listener);
    }

    public boolean removeScopeListener(ScopeListener listener) {
        return attrs().removeScopeListener(listener);
    }

    public String getAutag() {
        return _auxinf != null ? _auxinf.autag : null;
    }

    public void setAutag(String tag) {
        if (tag != null && Strings.isEmpty(tag))
            tag = null;
        if (!Objects.equals(_auxinf != null ? _auxinf.autag : null, tag)) {
            initAuxInfo().autag = tag;
            smartUpdate("autag", getAutag());
        }
    }

    public Component getParent() {
        return _parent;
    }

    public void setParent(Component parent) {
        if (_parent == parent)
            return; //nothing changed

        checkParentChild(parent, this); //create _chdinf
        beforeParentChanged(parent);
        triggerBeforeHostParentChanged(parent);

        final boolean idSpaceChanged = (parent != null ? spaceController(parent) : null) != (_parent != null
                ? spaceController(_parent) : _page);

        clearVirtualIdSpace(); //clear since it is being added to another
        if (idSpaceChanged)
            removeFromIdSpacesDown(this);

        //call removeChild and clear _parent
        final AbstractComponent op = _parent;
        if (op != null) {
            if (!op._chdinf.inRemoving(this)) {
                op._chdinf.markRemoving(this, true);
                try {
                    op.removeChild(this); //spec: call back removeChild
                } finally {
                    op._chdinf.markRemoving(this, false);
                }
            }
            _parent = null; //op.removeChild assumes _parent not changed yet
        } else {
            if (_page != null)
                ((AbstractPage) _page).removeRoot(this); //Not depends on uuid
        }

        //call insertBefore and set _parent
        if (parent != null) {
            final AbstractComponent np = (AbstractComponent) parent;
            if (!np._chdinf.inAdding(this)) {
                np._chdinf.markAdding(this, true);
                try {
                    np.insertBefore(this, null); //spec: call back inserBefore
                } finally {
                    np._chdinf.markAdding(this, false);
                }
            }
            _parent = np; //np.insertBefore assumes _parent not changed yet
        } //if parent == null, assume no page at all (so no addRoot)

        //correct _page
        final Page newpg = _parent != null ? _parent.getPage() : null, oldpg = _page;
        addMoved(op, _page, newpg); //Not depends on UUID
        try {
            ComponentsCtrl.setRootParent(parent != null ? parent : op);
            setPage0(newpg); //UUID might be changed here
        } finally {
            ComponentsCtrl.setRootParent(null);
        }

        if (_auxinf != null && _auxinf.attrs != null)
            _auxinf.attrs.notifyParentChanged(_parent != null ? _parent : (Scope) _page);
        if (idSpaceChanged)
            addToIdSpacesDown(this); //called after setPage

        onParentChanged(parent);
        //call back UiLifeCycle
        afterComponentPageChanged(newpg, oldpg);
        if (newpg != null || oldpg != null) {
            final Desktop desktop = (oldpg != null ? oldpg : newpg).getDesktop();
            if (desktop != null) {
                ((DesktopCtrl) desktop).afterComponentMoved(parent, this, op);
                desktop.getWebApp().getConfiguration().afterComponentMoved(parent, this, op);
            }
        }
    }

    private void beforeComponentPageChanged(Page page) {
        //ZK-1148, add a @destroy annotation method.
        if (page == null)
            WebApps.getCurrent().getConfiguration().invokeCallback("destroy", this);
    }
    private void afterComponentPageChanged(Page newpg, Page oldpg) {
        if (newpg == oldpg) {
            // Orphan case: detached again after the parent was detached
            if (getParent() == null && oldpg == null) {
                WebApps.getCurrent().getConfiguration().afterComponentDetached(this, null);
            }
            return;
        }

        final Desktop desktop = (oldpg != null ? oldpg : newpg).getDesktop();
        if (desktop == null)
            return; //just in case

        //Note: if newpg and oldpg both non-null, they must be the same
        if (oldpg != null) {
            ((DesktopCtrl) desktop).afterComponentDetached(this, oldpg);
            desktop.getWebApp().getConfiguration().afterComponentDetached(this, oldpg);
        } else {
            ((DesktopCtrl) desktop).afterComponentAttached(this, newpg);
            desktop.getWebApp().getConfiguration().afterComponentAttached(this, newpg);
        }
    }

    /**
     * Checks the parent-child relation.
     * Notice it will create parent._chdinf
     * @param parent the parent (will-be). It may be null.
     * @param child the child (will-be). It cannot be null.
     */
    private static void checkParentChild(Component parent, Component child) throws UiException {
        if (child == null)
            throw new UiException("Child cannot be null");
        if (parent != null) {
            final AbstractComponent acp = (AbstractComponent) parent;
            if (acp.initChildInfo().inAdding(child))
                return; //check only once

            if (Components.isAncestor(child, parent))
                throw new UiException("A child cannot be a parent of its ancestor: " + child);
            if (!acp.isChildable())
                throw new UiException("Child not allowed in " + parent.getClass().getName());

            final Page parentpg = parent.getPage(), childpg = child.getPage();
            if (parentpg != null && childpg != null && parentpg.getDesktop() != childpg.getDesktop())
                throw new UiException("The parent and child must be in the same desktop: " + parent);

            final Component oldparent = child.getParent();
            if (spaceOwnerNoVirtual(parent) != (oldparent != null ? spaceOwnerNoVirtual(oldparent) : childpg))
                checkIdSpacesDown(child, parent);
        } else {
            final Page childpg = child.getPage();
            if (childpg != null)
                checkDetach(childpg);
        }
    }

    public boolean insertBefore(Component newChild, Component refChild) {
        if ((newChild instanceof Macro) && ((Macro) newChild).isInline())
            return ((Macro) newChild).setInlineParent(this, refChild);

        checkParentChild(this, newChild); ///create _chdinf

        if (refChild != null && refChild.getParent() != this)
            refChild = null;

        if (newChild == refChild)
            return false; //nothing changed (Listbox and other assumes this)

        beforeChildAdded(newChild, refChild);
        triggerBeforeHostChildAdded(newChild, refChild);

        final AbstractComponent nc = (AbstractComponent) newChild;
        final boolean moved = nc._parent == this; //moved in the same parent
        if (moved) {
            if (nc._next == refChild)
                return false; //nothing changed
            nc.addMoved(this, _page, _page);

            //detach from original place
            setNext(nc._prev, nc._next);
            setPrev(nc._next, nc._prev);
        } else { //new added
            //Note: call setParent to detach nc from old parent, if any,
            //before maintaining nc's _next, _prev...
            if (!_chdinf.inAdding(nc)) {
                _chdinf.markAdding(nc, true);
                try {
                    nc.setParent(this); //spec: callback setParent
                } finally {
                    _chdinf.markAdding(nc, false);
                }
            } else {
                nc._parent = this;
                //Set it since deriving class might assume parent is correct
                //after insertBefore. For example, Tabs.insertBefore().
                //
                //However, we don't call setPage0 and other here,
                //since the codes will become too complex.
                //In other words, when super.insertBefore() returns in a
                //deriving class, _parent is correct but _page may or may not
            }
        }

        if (refChild != null) {
            final AbstractComponent ref = (AbstractComponent) refChild;
            setNext(nc, ref);
            setPrev(nc, ref._prev);
            setNext(ref._prev, nc);
            setPrev(ref, nc);
        } else {
            if (_chdinf.last == null) {
                _chdinf.first = _chdinf.last = nc;
                nc._next = nc._prev = null;
            } else {
                _chdinf.last._next = nc;
                nc._prev = _chdinf.last;
                nc._next = null;
                _chdinf.last = nc;
            }
        }

        ++_chdinf.modCntChd;
        if (!moved) { //new added
            ++_chdinf.nChild;
            onChildAdded(nc);
            triggerAfterHostChildAdded(nc);
        }
        return true;
    }

    /** Set the next sibling of the given child. (this is a parent of comp). */
    /*package*/ final void setNext(AbstractComponent comp, AbstractComponent next) {
        if (comp != null)
            comp._next = next;
        else
            _chdinf.first = next;
    }

    /** Set the previous sibling of the given child. (this is a parent of comp). */
    /*package*/ final void setPrev(AbstractComponent comp, AbstractComponent prev) {
        if (comp != null)
            comp._prev = prev;
        else
            _chdinf.last = prev;
    }

    /** Increases the number of children. It assumes _chdinf not null. */
    /*package*/ final void incNChild(int diff) {
        _chdinf.nChild += diff;
    }

    /** Replace the specified component with this component in
     * the component tree. In other words, the parent of the given
     * component will become the parent of this components, so
     * are siblings and children. Furthermore, comp will be detached
     * at the end.
     *
     * <p>Notice that the replacement won't change anything at the client.
     * It is the caller'job to maintain the consistency between the server
     * and the client.
     *
     * <p>This method is rarely used.
     *
     * @param comp the component. In this implementation it supports
     * only derived classes of {@link AbstractComponent}.
     * @param bFellow whether to add this component to the map of fellows
     * if it is assigned with an ID. If false, the component (comp) cannot
     * be retrieved back even with an ID (note: ID is always preserved).
     * @param bListener whether to retain the event listeners and handlers.
     * If true, the event listeners and handlers, if any, will be registered
     * to this stub component. In other words, the event will be processed.
     * However, it is a stub component, rather than the original one.
     * I means the event is the most generic format: an instance of
     * {@link org.zkoss.zk.ui.event.Event} (rather than MouseEvent or others).
     * @param bChildren whether to have the children of the given component.<br/>
     * If false, this component won't have any children, and all UUID of children
     * reference back to this component.<br/>
     * If true, the given component's children will belong to this component.
     * @exception IllegalStateException if this component has a parent,
     * sibling or child.
     * @since 6.0.0
     */
    protected void replace(Component comp, boolean bFellow, boolean bListener, boolean bChildren) {
        ((AbstractComponent) comp).replaceWith(this, bFellow, bListener, bChildren);
    }

    private final void replaceWith(AbstractComponent comp, boolean bFellow, boolean bListener, boolean bChildren) {
        if (this == comp || comp._parent != null || comp._next != null || comp._prev != null || comp._chdinf != null
                || comp._page != null)
            throw new IllegalStateException();

        comp._def = _def;
        comp._uuid = _uuid;

        //remove this from the fellow map
        removeFromIdSpaces(this); //call before changing _parent...

        //fix parent/sibling link
        AbstractComponent p = comp._parent = _parent, q = comp._prev = _prev;
        if (q != null)
            q._next = comp;
        else if (p != null)
            p._chdinf.first = comp;

        q = comp._next = this._next;
        if (q != null)
            q._prev = comp;
        else if (p != null)
            p._chdinf.last = comp;

        _parent = _next = _prev = null;

        //fix the uuid-to-component map
        final Page page;
        if ((page = _page) != null) {
            comp._page = page;
            if (comp._parent == null)
                ((AbstractPage) page).onReplaced(this, comp);
            //call onReplaced instead addRoot/removeRoot

            final DesktopCtrl desktopCtrl = (DesktopCtrl) page.getDesktop();
            desktopCtrl.mapComponent(_uuid, comp);
            _page = null;
        }

        //add comp to the fellow map
        comp._id = _id;
        if (bFellow)
            addToIdSpaces(comp); ///called after fixing comp._parent...

        if (_auxinf != null)
            comp._auxinf = _auxinf.cloneStub(comp, bListener);

        //fix children link; do it as the last step,
        //since StubsComponent.onChildrenMerge depends on _page
        if (_chdinf != null)
            if (bChildren) {
                for (p = _chdinf.first; p != null; p = p._next)
                    p._parent = comp;
                comp._chdinf = _chdinf;
                _chdinf = null;
            } else if (comp instanceof StubsComponent) { //dirty but not worth to generalize it yet
                ((StubsComponent) comp).onChildrenMerged(this, bListener);
            } else if (page != null) {
                childrenMerged((DesktopCtrl) page.getDesktop(), _chdinf);
            }
    }

    private static void childrenMerged(DesktopCtrl desktopCtrl, ChildInfo chdinf) {
        if (chdinf != null)
            for (AbstractComponent p = chdinf.first; p != null; p = p._next) {
                desktopCtrl.removeComponent(p);
                childrenMerged(desktopCtrl, p._chdinf);
            }
    }

    /** Appends a child to the end of all children.
     * It calls {@link #insertBefore} with refChild to be null.
     * Derives cannot override this method, and they shall override
     * {@link #insertBefore} instead.
     */
    public boolean appendChild(Component child) { //Yes, final; see below
        return insertBefore(child, null); //NOTE: we must go thru insertBefore
        //such that deriving is easy to override
    }

    public boolean removeChild(Component child) {
        final AbstractComponent oc = (AbstractComponent) child;
        if (oc._parent != this)
            return false; //nothing to do

        beforeChildRemoved(child);
        triggerBeforeHostChildRemoved(child);

        setNext(oc._prev, oc._next);
        setPrev(oc._next, oc._prev);
        oc._next = oc._prev = null;

        if (!_chdinf.inRemoving(oc)) {
            _chdinf.markRemoving(oc, true);
            try {
                oc.setParent(null); //spec: call back setParent
            } finally {
                _chdinf.markRemoving(oc, false);
            }
        } else {
            oc._parent = null;
            //Correct it since deriving class might assume parent is
            //correct after insertBefore() returns.
            //refer to insertBefore for more info.
        }

        //ZK-725: we can't remove _chdinf even if no child at all,
        //since this method might be called by ChildIter.remove()
        ++_chdinf.modCntChd;
        --_chdinf.nChild;
        onChildRemoved(child);
        triggerAfterHostChildRemoved(child);
        return true;
    }

    /** Returns whether this component can have a child.
     * <p>Default: return true (means it can have children).
     */
    protected boolean isChildable() {
        return true;
    }

    /** Returns a live list of children.
     * By live we mean the developer could add or remove a child by manipulating the returned list directly.
     * <p>Default: instantiates and returns an instance of {@link Children}.
     */
    public <T extends Component> List<T> getChildren() {
        return (List<T>) new Children();
    }

    /** Returns the root of the specified component.
     */
    public Component getRoot() {
        for (Component comp = this;;) {
            final Component parent = comp.getParent();
            if (parent == null)
                return comp;
            comp = parent;
        }
    }

    public boolean isVisible() {
        return _auxinf == null || _auxinf.visible;
    }

    public boolean setVisible(boolean visible) {
        final boolean old = _auxinf == null || _auxinf.visible;
        if (old != visible) {
            initAuxInfo().visible = visible;
            smartUpdate("visible", _auxinf.visible);
        }
        return old;
    }

    /** Changes the visibility directly without sending any update to the client.
     * It is the caller's responsibility to maintain the consistency.
     * It is rarely called. In most cases, you shall use {@link #setVisible} instead.
     * @since 5.0.4
     */
    protected void setVisibleDirectly(boolean visible) {
        initAuxInfo().visible = visible;
    }

    public String getStubonly() {
        final int v = _auxinf != null ? _auxinf.stubonly : 0;
        return v == 0 ? "inherit" : v < 0 ? "false" : "true";
    }

    public void setStubonly(String stubonly) {
        int v;
        if ("false".equals(stubonly))
            v = -1;
        else if ("true".equals(stubonly))
            v = 1;
        else if ("inherit".equals(stubonly))
            v = 0;
        else
            throw new UiException("Not allowed: " + stubonly);

        if ((_auxinf != null ? _auxinf.stubonly : 0) != v)
            initAuxInfo().stubonly = (byte) v;
        //no need to update client (it is all about server-side handling)
    }

    public void setStubonly(boolean stubonly) {
        setStubonly(stubonly ? "true" : "false");
    }

    public boolean isInvalidated() {
        return _page == null || getAttachedUiEngine().isInvalidated(this);
    }

    public void invalidate() {
        if (_page != null) {
            getAttachedUiEngine().addInvalidate(this);
        }
    }

    /** Causes a response to be sent to the client.
     * It is the same as <code>response(response.getOverrideKey(), response)</code>
     *
     * <p>If {@link AuResponse#getDepends} is not null, the response
     * depends on the existence of the component returned by
     * {@link AuResponse#getDepends}.
     * In other words, the response is removed if the component is removed.
     * If it is null, the response is component-independent and it is
     * always sent to the client.
     *
     * <p>Unlike {@link #smartUpdate}, responses are sent even if
     * {@link Component#invalidate()} was called.
     * Typical examples include setting the focus, selecting the text and so on.
     *
     * <p>It can be called only in the request-processing and event-processing
     * phases; excluding the redrawing phase.
     *
     * @since 5.0.2
     * @see #response(String, AuResponse)
     */
    protected void response(AuResponse response) {
        response(response.getOverrideKey(), response);
    }

    /** Causes a response to be sent to the client by overriding the key
     * returned by {@link AuResponse#getOverrideKey}).
     *
     * <p>If {@link AuResponse#getDepends} is not null, the response
     * depends on the existence of the component returned by
     * {@link AuResponse#getDepends}.
     * In other words, the response is removed if the component is removed.
     * If it is null, the response is component-independent and it is
     * always sent to the client.
     *
     * <p>Unlike {@link #smartUpdate}, responses are sent even if
     * {@link Component#invalidate()} was called.
     * Typical examples include setting the focus, selecting the text and so on.
     *
     * <p>It can be called only in the request-processing and event-processing
     * phases; excluding the redrawing phase.
     *
     * @param key could be anything.
     * The second invocation of this method
     * in the same execution with the same key and the same depends
     * ({@link AuResponse#getDepends}) will override the previous one.
     * However, if key is null, it won't override any other. All responses
     * with key == null will be sent.<br/>
     * Notice that if {@link AuResponse#getDepends} is null, then be careful
     * of the key you used since it is shared in the same execution
     * (rather than a particular component).
     * @since 5.0.0 (become protected)
     */
    protected void response(String key, AuResponse response) {
        response(key, response, 0);
    }

    /** Causes a response to be sent to the client by overriding the key
     * returned by {@link AuResponse#getOverrideKey}).
     *
     * <p>If {@link AuResponse#getDepends} is not null, the response
     * depends on the existence of the component returned by
     * {@link AuResponse#getDepends}.
     * In other words, the response is removed if the component is removed.
     * If it is null, the response is component-independent and it is
     * always sent to the client.
     *
     * <p>Unlike {@link #smartUpdate}, responses are sent even if
     * {@link Component#invalidate()} was called.
     * Typical examples include setting the focus, selecting the text and so on.
     *
     * <p>It can be called only in the request-processing and event-processing
     * phases; excluding the redrawing phase.
     *
     * @param key could be anything.
     * The second invocation of this method
     * in the same execution with the same key and the same depends
     * ({@link AuResponse#getDepends}) will override the previous one.
     * However, if key is null, it won't override any other. All responses
     * with key == null will be sent.<br/>
     * Notice that if {@link AuResponse#getDepends} is null, then be careful
     * of the key you used since it is shared in the same execution
     * (rather than a particular component).
     * @param priority The higher priority, the earlier the update is executed.
     * The priority of {@link #response(AuResponse)}
     *  and {@link #response(String, AuResponse)} is assumed to be 0.
     * <p>If the priority is the same, the update is executed in the order
     * of first-in-first out.
     * @since 6.0.1
     */
    protected void response(String key, AuResponse response, int priority) {
        //if response not depend on this component, it must be generated
        if (_page != null) {
            getAttachedUiEngine().addResponse(key, response, priority);
        } else if (response.getDepends() != this) {
            final UiEngine uieng = getCurrentUiEngine();
            if (uieng != null)
                uieng.addResponse(key, response);
        }
    }

    /** Smart-updates a property of the peer widget associated with
     * the component, running at the client, with the given value.
     *
     * <p>The second invocation with the same property will replace the previous
     * call. In other words, the same property will be set only once in
     * each execution. If you prefer to send both updates to the client,
     * use {@link #smartUpdate(String, Object, boolean)} instead.
     *
     * <p>This method has no effect if {@link #invalidate()} is ever invoked
     * (in the same execution), since {@link #invalidate()} assumes
     * the whole content shall be redrawn and all smart updates to
     * this components can be ignored,
     *
     * <p>Once {@link #invalidate} is called, all invocations to {@link #smartUpdate(String, Object)}
     * will then be ignored, and {@link #redraw} will be invoked later.
     *
     * <p>It can be called only in the request-processing and event-processing
     * phases; excluding the redrawing phase.
     *
     * <p>There are two ways to draw a component, one is to invoke
     * {@link Component#invalidate()}, and the other is {@link #smartUpdate(String, Object)}.
     * While {@link Component#invalidate()} causes the whole content to redraw,
     * {@link #smartUpdate(String, Object)} let component developer control which part
     * to redraw.
     *
     * @param value the new value.
     * If it is {@link org.zkoss.zk.au.DeferredValue}, the value
     * will be retrieved (by calling {@link org.zkoss.zk.au.DeferredValue#getValue})
     * in the rendering phase. It is useful if the value can not be determined now.
     * <p>For some old application servers (example, Websphere 5.1),
     * {@link Execution#encodeURL} cannot be called in the event processing
     * thread. So, the developers have to use {@link org.zkoss.zk.au.DeferredValue}
     * or disable the use of the event processing thread
     * (by use of <code>disable-event-thread</code> in zk.xml).
     * <p>If you want to generate the JavaScript code directly (i.e.,
     * the value is a valid JavaScript snippet), you can use
     * {@link JavaScriptValue}. Notice that the JavaScript code will be evaluated
     * before assigning it to the widget.
     * <p>If the value is a Date object, a special pattern will be generated
     * (a.k.a., marshaling)
     * to ensure it can be unmarshalled back correctly at the client.
     * Notice that it is marshalled to a string based
     * on {@link org.zkoss.util.TimeZones#getCurrent}, and then
     * unmarshalled back at the client. In other words, if the client
     * is in different time-zone, the value returned by getTime() might
     * be different. However, the value will remain the same if
     * the client marshalled the Date object back.
     * In other words, it assumes the browser's time zone from enduser's
     * perspective (not really browser's setting) shall be the same
     * as {@link org.zkoss.util.TimeZones#getCurrent}.
     * <p>If the value is a component, a special pattern will be generated
     * to ensure it can be unmarshalled back correctly at the client.
     * <p>In addition, the value can be any kind of objects that
     * the client accepts (marshaled by JSON) (see also {@link org.zkoss.json.JSONAware}).
     * @since 5.0.0 (become protected)
     * @see #updateByClient
     * @see #smartUpdate(String, Object, boolean)
     */
    protected void smartUpdate(String attr, Object value) {
        smartUpdate(attr, value, false);
    }

    /** Smart-updates a property of the peer widget with the given value
     * that allows caller to decide whether to append or overwrite.
     * In other words, {@link #smartUpdate(String, Object)} is a shortcut of
     * <code>smartUpdate(attr, value, false)</code>.
     *
     * <p>For example, if you invoke <code>smartUpdate("attr", "value1")</code>
     * and <code>smartUpdate("attr", "value2")</code>, then only <code>value2</code>
     * will be sent to the client.
     * <p>However, if you invoke <code>smartUpdate("attr", "value1", true)</code>
     * and <code>smartUpdate("attr", "value2", true)</code>,
     * then both <code>value1</code> and <code>value2</code>
     * will be sent to the client. In other words, <code>wgt.setAttr("value1")</code>
     * and <code>wgt.setAttr("value2")</code> will be invoked at the client
     * accordingly.
     *
     * @param append whether to append the updates of properties with the same
     * name. If false, only the last value of the same property will be sent
     * to the client.
     * @since 5.0.0
     * @see #smartUpdate(String, Object)
     */
    protected void smartUpdate(String attr, Object value, boolean append) {
        if (_page != null)
            getAttachedUiEngine().addSmartUpdate(this, attr, value, append);
    }

    /** A special smart update to update a value in int.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, int value) {
        smartUpdate(attr, new Integer(value));
    }

    /** A special smart update to update a value in long.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, long value) {
        smartUpdate(attr, new Long(value));
    }

    /** A special smart update to update a value in byte.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, byte value) {
        smartUpdate(attr, new Byte(value));
    }

    /** A special smart update to update a value in character.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, char value) {
        smartUpdate(attr, new Character(value));
    }

    /** A special smart update to update a value in boolean.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, boolean value) {
        smartUpdate(attr, Boolean.valueOf(value));
    }

    /** A special smart update to update a value in float.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, float value) {
        smartUpdate(attr, new Float(value));
    }

    /** A special smart update to update a value in double.
     * <p>It is the same as {@link #smartUpdate(String, Object)}.
     * @since 5.0.0
     */
    protected void smartUpdate(String attr, double value) {
        smartUpdate(attr, new Double(value));
    }

    /** A special smart update to update an event listener for the
     * peer widget.
     * By default, it assumes the peer widget has a method called
     * <code>setListener<code> and it will be invoked as follows.
     *
     * <pre><code>wgt.setListener([evtnm, script]);</code></pre>
     *
     * <p>Devices that supports it in another way have to override this
     * method. Devices that don't support it have to override this method
     * to throw UnsupportedOperationException.
     *
     * @param evtnm the event name, such as onClick
     * @param script the script. If null, it means to remove the event
     * listener from the peer widget
     * @since 5.0.0
     */
    protected void smartUpdateWidgetListener(String evtnm, String script) {
        smartUpdate("listener", new String[] { evtnm, script }, true);
    }

    /** A special smart update to update a method or a field of the peer widget.
     * By default, it invokes the client widget's <code>setOverride</code> as follows.
     *
     * <pre><code>wgt.setOverride([name: script]);</code></pre>
     *
     * <p>Devices that supports it in another way have to override this
     * method. Devices that don't support it have to override this method
     * to throw UnsupportedOperationException.
     *
     * @param name the method name, such as setValue
     * @param script the content of the method or field to override.
     * Notice that it must be a valid JavaScript snippet.
     * If null, the previous method/field override
     * will be removed. And, the method/field defined in original widget will
     * be restored.
     * @since 5.0.0
     */
    protected void smartUpdateWidgetOverride(String name, String script) {
        smartUpdate("override", new Object[] { name, new JavaScriptValue(script) }, true);
    }

    public void detach() {
        if (getParent() != null)
            setParent(null);
        else
            setPage(null);
    }

    /** Default: does nothing.
     * @see ComponentCtrl#beforeChildAdded
     * @since 3.6.2
     */
    public void beforeChildAdded(Component child, Component insertBefore) {
    }

    /** Default: does nothing.
     * @see ComponentCtrl#beforeChildRemoved
     * @since 3.6.2
     */
    public void beforeChildRemoved(Component child) {
    }

    /** Default: If parent is null, execute the @Destroy method if any.
     * @see ComponentCtrl#beforeParentChanged
     * @since 3.6.2
     */
    public void beforeParentChanged(Component parent) {
        //ZK-1148, add a @destroy annotation method.
        if (parent == null)//detach
            WebApps.getCurrent().getConfiguration().invokeCallback("destroy", this);
        disableBindingAnnotation();
    }

    public void onParentChanged(Component parent) {
        processCallback(AFTER_PARENT_CHANGED);
        enableBindingAnnotation();
    }

    /** Default: handles special event listeners.
     * @see ComponentCtrl#onChildAdded
     */
    public void onChildAdded(Component child) {
        processCallback(AFTER_CHILD_ADDED);
    }

    /** Default: handles special event listeners.
     * @see ComponentCtrl#onChildRemoved
     */
    public void onChildRemoved(Component child) {
        processCallback(AFTER_CHILD_REMOVED);
    }

    /** Default: handles special event listeners.
     * @see ComponentCtrl#onPageAttached
     * @since 3.0.0
     */
    public void onPageAttached(Page newpage, Page oldpage) {
        if (oldpage == null) //new added
            onListenerChange(newpage.getDesktop(), true);
        processCallback(AFTER_PAGE_ATTACHED);
    }

    /** Default: handles special event listeners.
     * @see ComponentCtrl#onPageDetached
     * @since 3.0.0
     */
    public void onPageDetached(Page page) {
        onListenerChange(page.getDesktop(), false);
        processCallback(AFTER_PAGE_DETACHED);
    }

    private void processCallback(String name) {
        Collection<Callback> callbacks = getCallback(name);
        for (Callback callback : new ArrayList<Callback>(callbacks)) {
            callback.call(this);
            removeCallback(name, callback);
        }
    }

    /** Returns the widget class (a.k.a., widget type), or null if not defined.
     * <p>Default: return the widget class based on the current mold
     * (by use of {@link ComponentDefinition#getWidgetClass}), or null
     * if not found.
     * <p>To override in Java, you could invoke {@link #setWidgetClass}.
     * To override in ZUML, you could use the client namespace as follows.
     * <pre><code>
    &lt;window xmlns:w="http://www.zkoss.org/2005/zk/client"
    w:use="foo.MyWindow"&gt;
    &lt;/window&gt;
     *</code></pre>
     * @since 5.0.0
     */
    public String getWidgetClass() {
        if (_auxinf != null && _auxinf.wgtcls != null)
            return _auxinf.wgtcls;
        final String widgetClass = _def.getWidgetClass(this, getMold());
        return widgetClass != null ? widgetClass : _def.getDefaultWidgetClass(this);
    }

    public void setWidgetClass(String wgtcls) {
        if (wgtcls != null && wgtcls.length() > 0) {
            initAuxInfo().wgtcls = wgtcls;
        } else if (_auxinf != null) {
            _auxinf.wgtcls = null;
        }
    }

    public String getMold() {
        final String mold = _auxinf != null ? _auxinf.mold : null;
        return mold != null ? mold : DEFAULT;
    }

    public void setMold(String mold) {
        if (mold != null && (DEFAULT.equals(mold) || mold.length() == 0))
            mold = null;
        if (!Objects.equals(_auxinf != null ? _auxinf.mold : DEFAULT, mold)) {
            if (!_def.hasMold(mold != null ? mold : DEFAULT))
                throw new UiException("Unknown mold: " + mold + "; allowed: " + _def.getMoldNames());
            final String oldtype = getWidgetClass();
            initAuxInfo().mold = mold;
            if (Objects.equals(oldtype, getWidgetClass()))
                smartUpdate("mold", getMold());
            else
                invalidate();
        }
    }

    protected String getSpecialRendererOutput(Component comp) throws IOException {
        if (_page != null) {
            final JsContentRenderer renderer = new JsContentRenderer();
            PropertiesRenderer[] prs = _page.getDesktop().getWebApp().getConfiguration().getPropertiesRenderers();
            for (int j = 0; j < prs.length; j++)
                prs[j].renderProperties(comp, renderer);
            return renderer.getBuffer().toString();
        }
        return "";
    }

    public boolean disableClientUpdate(boolean disable) {
        final UiEngine uieng = _page != null ? getAttachedUiEngine() : getCurrentUiEngine();
        return uieng != null && uieng.disableClientUpdate(this, disable);
    }

    public boolean addRedrawCallback(Callback<ContentRenderer> callback) {
        return addCallback("redraw", callback);
    }

    public boolean removeRedrawCallback(Callback<ContentRenderer> callback) {
        return removeCallback("redraw", callback);
    }

    public Collection<Callback<ContentRenderer>> getRedrawCallback() {
        return cast(getCallback("redraw"));
    }

    public boolean addCallback(String name, Callback callback) {
        if (callback == null)
            throw new IllegalArgumentException();

        if (initAuxInfo().callbacks == null)
            _auxinf.callbacks = new LinkedHashMap<String, List<Callback<?>>>(2);
        List<Callback<?>> list = _auxinf.callbacks.get(name);
        if (list == null) {
            list = new LinkedList<Callback<?>>();
            _auxinf.callbacks.put(name, list);
        }
        return list.add(callback);
    }

    public boolean removeCallback(String name, Callback callback) {
        if (initAuxInfo().callbacks != null) {
            List<Callback<?>> list = _auxinf.callbacks.get(name);
            if (list != null)
                return list.remove(callback);
        }
        return false;
    }

    public Collection<Callback> getCallback(String name) {
        if (initAuxInfo().callbacks != null) {
            List<Callback<?>> list = _auxinf.callbacks.get(name);
            if (list != null)
                return cast(list);
        }
        return Collections.emptyList();
    }

    //-- in the redrawing phase --//
    /** Redraws this component and all its descendants.
     * <p>Default: It uses {@link JsContentRenderer} to render all information
     * in JavaScript codes. For devices that don't support JavaScript,
     * it must override this method.
     * <p>To generate all information, it first invokes
     * {@link #renderProperties} to render component's
     * properties,
     * and  then {@link #redrawChildren} to redraw children (and descendants)
     * (by calling their {@link #redraw}).
     *
     * <p>If a derived class wants to render more properties, it can override
     * {@link #renderProperties}.
     * <p>If a derived class renders only a subset of its children
     * (such as paging/cropping), it could override {@link #redrawChildren}.
     * <p>If a deriving class wants to do something before
     * {@link #renderProperties}, it has to override {@link #redraw}.
     * <p>If a deriving class doesn't want to render in JavaScript codes,
     * it has to override {@link #redraw} with the proper implementation
     * of {@link ContentRenderer}.
     */
    public void redraw(final Writer out) throws IOException {
        final int order = ComponentRedraws.beforeRedraw(false);
        final boolean aupg = isAsyncUpdate();
        final String extra;
        try {
            if (order < 0) {
                if (aupg)
                    out.write('[');
                else {
                    out.write(HtmlPageRenders.outSpecialJS(getDesktop()));
                    out.write("zkx(");
                }
            } else if (order > 0) //not first child
                out.write(',');

            final JsContentRenderer renderer = new JsContentRenderer();
            renderProperties(renderer);
            if (_page != null) {
                PropertiesRenderer[] prs = _page.getDesktop().getWebApp().getConfiguration().getPropertiesRenderers();
                for (int j = 0; j < prs.length; j++)
                    prs[j].renderProperties(this, renderer);
            }

            // support a way to callback for shadow element
            JSCumulativeContentRenderer serenderer = null;

            Collection<Callback<ContentRenderer>> redrawCallback = getRedrawCallback();

            if (!redrawCallback.isEmpty()) {
                serenderer = new JSCumulativeContentRenderer();
                for (Callback<ContentRenderer> callback : redrawCallback) {
                    callback.call(serenderer);
                }
            }

            final String wgtcls = getWidgetClass();
            if (wgtcls == null)
                throw new UiException("Widget class required for " + this + " with " + getMold());
            out.write("\n['");
            out.write(wgtcls);
            out.write("','");
            out.write(getUuid());
            out.write("',{");
            out.write(renderer.getBuffer().toString());
            out.write("},{");
            out.write(serenderer == null ? "" : serenderer.toString());
            out.write("},[");

            redrawChildren(out);

            out.write(']');
            final String mold = getMold();
            if (!DEFAULT.equals(mold)) {
                out.write(",'");
                out.write(mold);
                out.write('\'');
            }
            out.write(']');

        } finally {
            extra = ComponentRedraws.afterRedraw();
        }
        if (order < 0) {
            if (aupg) {
                if (extra.length() > 0) {
                    out.write(",0,null,'");
                    out.write(Strings.escape(extra, Strings.ESCAPE_JAVASCRIPT));
                    out.write('\'');
                }
                out.write(']');
            } else {
                if (extra.length() > 0)
                    out.write(",1"); //Bug 2983792 (delay until non-defer script evaluated)
                out.write(");\n");
                out.write(extra);
            }
        }
    }

    private final boolean isAsyncUpdate() {
        final Execution exec = Executions.getCurrent();
        return exec != null && exec.isAsyncUpdate(_page);
    }

    /** Redraws children (and then recursively descendants).
     * <p>Default: it invokes {@link #redraw} for all its children.
     * <p>If a derived class renders only a subset of its children
     * (such as paging/cropping), it could override {@link #redrawChildren}.
     * @since 5.0.0
     * @see #redraw
     */
    protected void redrawChildren(Writer out) throws IOException {
        final Object xc = getExtraCtrl();
        if (xc instanceof Cropper) {
            final Set<? extends Component> crop = ((Cropper) xc).getAvailableAtClient();
            if (crop != null) {
                for (Component c : crop)
                    if (c.getParent() == this)
                        ((ComponentCtrl) c).redraw(out);
                //Note: getAvialableAtClient might return all level
                //of children in the same crop scope
                return;
            }
        }

        for (Component child = getFirstChild(); child != null;) {
            Component next = child.getNextSibling();
            ((ComponentCtrl) child).redraw(out);
            child = next;
        }
    }

    /** Called by ({@link ComponentCtrl#redraw}) to render the
     * properties, excluding the enclosing tag and children.
     *
     * <p>Default: it renders {@link #getId} if it was assigned,
     * and event names if listened (and listed in {@link #getClientEvents}).
     *
     * <p>Note: it doesn't render {@link #getWidgetClass}, {@link #getUuid}
     * and {@link #getMold}, which are caller's job.
     *
     * @since 5.0.0
     */
    protected void renderProperties(ContentRenderer renderer) throws IOException {
        _initialized = true;
        render(renderer, "id", _id);
        if (_auxinf != null && !_auxinf.visible) //don't call isVisible since it might be overriden (backward compatible)
            renderer.render("visible", false);
        render(renderer, "autag", getAutag());

        final Desktop desktop = getDesktop();
        if (this instanceof IdSpace && this.getAttribute("z$is") == null) // Used by Window and others to minimize number of bytes
            renderer.render("z$is", true);

        Boolean shallHandleImportant = null;
        for (Map.Entry<String, Integer> me : getClientEvents().entrySet()) {
            final String evtnm = me.getKey();
            final int flags = me.getValue().intValue();
            boolean isImportant = false;
            boolean isListened = false;
            if ((flags & CE_IMPORTANT) != 0) {
                if (shallHandleImportant == null)
                    shallHandleImportant = Boolean.valueOf(Utils.markClientInfoPerDesktop(desktop, getWidgetClass()));
                if (shallHandleImportant.booleanValue())
                    renderer.render("$$" + evtnm, (flags & CE_NON_DEFERRABLE) != 0);
                isImportant = true;
            }
            if (Events.isListened(this, evtnm, false)) {
                renderer.render('$' + evtnm, Events.isListened(this, evtnm, true));
                //$onClick and so on
                isListened = true;
            }
            //only render the following two types when event flag is important or listened
            if ((flags & CE_DUPLICATE_IGNORE) != 0 && (isImportant || isListened)) {
                renderer.render("$$0" + evtnm, true);
            }
            if ((flags & CE_REPEAT_IGNORE) != 0 && (isImportant || isListened)) {
                renderer.render("$$1" + evtnm, true);
            }
        }

        if (_auxinf != null)
            _auxinf.render(renderer);

        // feature ZK-2822
        Object o = getAttribute("$composer");
        if (o != null)
            renderer.render("$ZKAUS$", Boolean.TRUE);

        o = getAttribute(Attributes.CLIENT_ROD);
        if (o != null)
            renderer.render("z$rod", (o instanceof Boolean && ((Boolean) o).booleanValue()) || !"false".equals(o));
    }

    public void renderPropertiesOnly(ContentRenderer renderer)
            throws IOException {
        this.renderProperties(renderer);
    }

    /** An utility to be called by {@link #renderProperties} to
     * render a string-value property.
     * It ignores if value is null or empty.
     * If you want to render it even if null/empty, invoke
     * {@link ContentRenderer#render(String, String)} directly.
     * @since 5.0.0
     */
    protected void render(ContentRenderer renderer, String name, String value) throws IOException {
        if (value != null && value.length() > 0)
            renderer.render(name, value);
    }

    /** An utility to be called by {@link #renderProperties} to
     * render a string-value property.
     * It ignores if value is null.
     * If you want to render it even if null, invoke
     * {@link ContentRenderer#render(String, Object)} directly.
     * @since 5.0.0
     */
    protected void render(ContentRenderer renderer, String name, Object value) throws IOException {
        if (value instanceof String)
            render(renderer, name, (String) value);
        else if (value != null)
            renderer.render(name, value);
    }

    /** An utility to be called by {@link #renderProperties} to
     * render a boolean-value property if it is true.
     * If you want to render it no matter true or false, use
     * {@link ContentRenderer#render(String, boolean)} directly.
     * @since 5.0.0
     */
    protected void render(ContentRenderer renderer, String name, boolean value) throws IOException {
        if (value)
            renderer.render(name, true);
    }

    /** Returns a map of event information that the client might send to this component.
     * The key of the returned map is a String instance representing the event name,
     * and the value an integer representing the flags
     * (a combination of {@link #CE_IMPORTANT}, {@link #CE_NON_DEFERRABLE}, {@link #CE_BUSY_IGNORE},
     * {@link #CE_DUPLICATE_IGNORE} and {@link #CE_REPEAT_IGNORE}).
     * <p>Default: return the collection of events
     * added by {@link #getClientEvents}.
     *
     * <p>Rather than overriding this method, it is suggested
     * to invoke {@link #addClientEvent} in the <code>static</code> statement.
     * For example,
     * <pre><code>public MyComponent extend HtmlBasedComponent {
     *  static {
     *    addClientEvent(MyComponent.class, "onOpen", 0);
     *  }</code></pre>
     *
     * @since 5.0.0
     */
    public Map<String, Integer> getClientEvents() {
        for (Class cls = getClass(); cls != null; cls = cls.getSuperclass()) {
            Map<String, Integer> events = _clientEvents.get(cls);
            if (events != null)
                return events;
        }
        return Collections.emptyMap();
    }

    /** Adds an event that the client might send to the server.
     * {@link #addClientEvent} is usually called in the <code>static</code> clause
     * when the class is loaded. For example,
     * <pre><code>public class MyWidget extends HtmlBasedComponent {
     *  static {
     *    addClientEvent(MyWidget.class, "onFly", 0);
     *  }
     *...</code></pre>
     *
     * <p>For a programming language not easy to have the <code>static</code>
     * clause (such as Scala), {@link #addClientEvent} can be called in
     * the constructors. Notice that it is better not to add the client event
     * later than the constructor, since the derived classes will copy
     * the client events defined in the base class, when the first time
     * {@link #addClientEvent} is called with the class.
     *
     * <h3>Version History</h3>
     * <p>Since 5.0.4, it can be called in constructors
     * (in additions to the static clause). On other hand, it can only
     * be called in the static clause (executed when the class is loaded)
     * in the prior version.
     * @param cls the component's class (implementation class).
     * @param flags a combination of {@link #CE_IMPORTANT}, {@link #CE_NON_DEFERRABLE}
     * {@link #CE_BUSY_IGNORE}, {@link #CE_DUPLICATE_IGNORE}
     * and {@link #CE_REPEAT_IGNORE}.
     * @since 5.0.0
     */
    protected static void addClientEvent(Class<? extends Component> cls, String evtnm, int flags) {
        Map<String, Integer> events = _clientEvents.get(cls);

        if (events == null) {
            synchronized (cls.getClass()) {
                events = _clientEvents.get(cls);
                if (events == null) {
                    //for better performance, we pack all event names of super
                    //classes, though it costs more memory
                    events = new ConcurrentHashMap<>(8);
                    for (Class c = cls; c != null; c = c.getSuperclass()) {
                        final Map<String, Integer> evts = _clientEvents.get(c);
                        if (evts != null) {
                            events.putAll(evts);
                            break;
                        }
                    }
                    _clientEvents.put(cls, events);
                }
            }
        }

        events.put(evtnm, new Integer(flags));
    }

    //Event//
    public boolean addEventListener(String evtnm, EventListener<? extends Event> listener) {
        return addEventListener(0, evtnm, listener);
    }

    public boolean addEventListener(int priority, String evtnm, EventListener<? extends Event> listener) {
        if (evtnm == null || listener == null)
            throw new IllegalArgumentException("null");
        if (!Events.isValid(evtnm))
            throw new IllegalArgumentException("Invalid event name: " + evtnm);

        final boolean oldasap = Events.isListened(this, evtnm, true);

        if (initAuxInfo().listeners == null)
            _auxinf.listeners = new HashMap<String, List<EventListenerInfo>>(8);

        boolean found = false;
        List<EventListenerInfo> lis = _auxinf.listeners.get(evtnm);
        final EventListenerInfo listenerInfo = new EventListenerInfo(priority, listener);
        if (lis != null) {
            if (Impls.duplicateListenerIgnored()) {
                for (Iterator<EventListenerInfo> it = lis.iterator(); it.hasNext();) {
                    final EventListenerInfo li = it.next();
                    if (li.listener.equals(listener)) {
                        if (li.priority == priority)
                            return false; //nothing to do
                        it.remove(); //re-added later
                        found = true;
                        break;
                    }
                }
            }

            for (ListIterator<EventListenerInfo> it = lis.listIterator(lis.size());;) {
                final EventListenerInfo li = it.hasPrevious() ? it.previous() : null;
                if (li == null || li.priority >= priority) {
                    if (li != null)
                        it.next();
                    it.add(listenerInfo);
                    break;
                }
            }
        } else {
            _auxinf.listeners.put(evtnm, lis = new LinkedList<EventListenerInfo>());
            lis.add(listenerInfo);
        }

        final Desktop desktop;
        if (!found && (desktop = getDesktop()) != null) {
            if (Events.ON_CLIENT_INFO.equals(evtnm)) {
                desktop.setAttribute("org.zkoss.desktop.clientinfo.enabled", true);
                response(new AuClientInfo(desktop));
            } else if (Events.ON_PIGGYBACK.equals(evtnm)) {
                ((DesktopCtrl) desktop).onPiggybackListened(this, true);
            } else if (Events.ON_VISIBILITY_CHANGE.equals(evtnm)) {
                desktop.setAttribute("org.zkoss.desktop.visibilitychange.enabled", true);
            } else if (getClientEvents().containsKey(evtnm)) {
                final boolean asap = Events.isListened(this, evtnm, true);
                if (lis.size() == 1 || oldasap != asap)
                    smartUpdate("$" + evtnm, asap);
            }
        }
        return !found;
    }

    public boolean removeEventListener(String evtnm, EventListener<? extends Event> listener) {
        if (evtnm == null || listener == null)
            throw new IllegalArgumentException("null");

        if (_auxinf != null && _auxinf.listeners != null) {
            final boolean oldasap = Events.isListened(this, evtnm, true);
            final List<EventListenerInfo> lis = _auxinf.listeners.get(evtnm);
            if (lis != null) {
                for (Iterator<EventListenerInfo> it = lis.iterator(); it.hasNext();) {
                    final EventListenerInfo li = it.next();
                    if (li.listener.equals(listener)) {
                        it.remove();
                        if (lis.isEmpty())
                            _auxinf.listeners.remove(evtnm);

                        final Desktop desktop = getDesktop();
                        if (desktop != null) {
                            onListenerChange(desktop, false);

                            if (getClientEvents().containsKey(evtnm)) {
                                if (lis.isEmpty() && !Events.isListened(this, evtnm, false))
                                    smartUpdate("$" + evtnm, (Object) null); //no listener at all
                                else if (oldasap != Events.isListened(this, evtnm, true))
                                    smartUpdate("$" + evtnm, !oldasap);
                            }
                        }
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public boolean addForward(String orgEvent, Component target, String targetEvent) {
        return addForward0(orgEvent, target, targetEvent, null);
    }

    public boolean addForward(String orgEvent, String targetPath, String targetEvent) {
        return addForward0(orgEvent, targetPath, targetEvent, null);
    }

    public boolean addForward(String orgEvent, Component target, String targetEvent, Object eventData) {
        return addForward0(orgEvent, target, targetEvent, eventData);
    }

    public boolean addForward(String orgEvent, String targetPath, String targetEvent, Object eventData) {
        return addForward0(orgEvent, targetPath, targetEvent, eventData);
    }

    /**
     * @param target the target. It is either a component, or a string,
     * which is used internal for implementing {@link #writeObject}
     */
    private boolean addForward0(String orgEvent, Object target, String targetEvent, Object eventData) {
        if (orgEvent == null)
            orgEvent = "onClick";
        else if (!Events.isValid(orgEvent))
            throw new IllegalArgumentException("Illegal event name: " + orgEvent);
        if (targetEvent == null)
            targetEvent = orgEvent;
        else if (!Events.isValid(targetEvent))
            throw new IllegalArgumentException("Illegal event name: " + targetEvent);

        if (initAuxInfo().forwards == null)
            _auxinf.forwards = new HashMap<String, ForwardInfo>(4);

        ForwardInfo info = _auxinf.forwards.get(orgEvent);
        final List<TargetInfo> tis;
        if (info != null) {
            tis = info.targets;
            for (TargetInfo ti : tis) {
                if (Objects.equals(ti.target, target) && Objects.equals(ti.event, targetEvent)) { //found
                    if (Objects.equals(ti.data, eventData)) {
                        return false;
                    } else {
                        ti.data = eventData;
                        return true;
                    }
                }
            }
        } else {
            final ForwardListener listener = new ForwardListener(orgEvent);
            addEventListener(orgEvent, listener);
            info = new ForwardInfo(listener, tis = new LinkedList<TargetInfo>());
            _auxinf.forwards.put(orgEvent, info);
        }

        tis.add(new TargetInfo(target, targetEvent, eventData));
        return true;
    }

    public Map<String, ForwardInfo> getForwards() {
        return _auxinf.forwards != null ? Collections.unmodifiableMap(_auxinf.forwards) : Collections.emptyMap();
    }

    public boolean removeForward(String orgEvent, Component target, String targetEvent) {
        return removeForward0(orgEvent, target, targetEvent);
    }

    public boolean removeForward(String orgEvent, String targetPath, String targetEvent) {
        return removeForward0(orgEvent, targetPath, targetEvent);
    }

    private boolean removeForward0(String orgEvent, Object target, String targetEvent) {
        if (_auxinf != null && _auxinf.forwards != null) {
            final ForwardInfo info = _auxinf.forwards.get(orgEvent);
            if (info != null) {
                final List<TargetInfo> tis = info.targets;
                for (Iterator<TargetInfo> it = tis.iterator(); it.hasNext();) {
                    final TargetInfo ti = it.next();
                    if (Objects.equals(ti.event, targetEvent) && Objects.equals(ti.target, target)) { //found
                        it.remove(); //remove it

                        if (tis.isEmpty()) { //no more event
                            _auxinf.forwards.remove(orgEvent);
                            removeEventListener(orgEvent, info.listener);
                        }
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public boolean isListenerAvailable(String evtnm, boolean asap) {
        if (_auxinf != null && _auxinf.listeners != null) {
            final List<EventListenerInfo> lis = _auxinf.listeners.get(evtnm);
            if (lis != null) {
                if (!asap)
                    return !lis.isEmpty();

                for (EventListenerInfo li : lis) {
                    if (!(li.listener instanceof Deferrable) || !(((Deferrable) li.listener).isDeferrable()))
                        return true;
                }
            }
        }
        return false;
    }

    public Iterable<EventListener<? extends Event>> getEventListeners(String evtnm) {
        if (_auxinf != null && _auxinf.listeners != null) {
            final List<EventListenerInfo> lis = _auxinf.listeners.get(evtnm);
            if (lis != null)
                return new Iterable<EventListener<? extends Event>>() {
                    public Iterator<EventListener<? extends Event>> iterator() {
                        return CollectionsX.comodifiableIterator(lis, _listenerInfoConverter);
                    }
                };
        }
        return CollectionsX.emptyIterable();
    }

    public void applyProperties() {
        _def.applyProperties(this);
    }

    public ComponentDefinition getDefinition() {
        return _def;
    }

    //-- ComponentCtrl --//
    public void setDefinition(ComponentDefinition compdef) {
        if (compdef == null)
            throw new IllegalArgumentException("null");
        if (!compdef.isInstance(this))
            throw new IllegalArgumentException("Incompatible " + compdef + " for " + this);
        _def = compdef;
    }

    public void setDefinition(String name) {
        final Execution exec = Executions.getCurrent();
        if (exec != null) {
            final ExecutionCtrl execCtrl = (ExecutionCtrl) exec;
            final PageDefinition pgdef = execCtrl.getCurrentPageDefinition();
            final Page page = execCtrl.getCurrentPage();

            ComponentDefinition compdef = pgdef != null ? pgdef.getComponentDefinition(name, true)
                    : page != null ? page.getComponentDefinition(name, true) : null;
            if (compdef == null)
                compdef = Impls.getDefinitionByDeviceType(this, exec.getDesktop().getDeviceType(), name);
            if (compdef != null) {
                setDefinition(compdef);
                return;
            }
        } else {
            for (String deviceType : LanguageDefinition.getDeviceTypes()) {
                final ComponentDefinition compdef = Impls.getDefinitionByDeviceType(this, deviceType, name);
                if (compdef != null) {
                    setDefinition(compdef);
                    return;
                }
            }
        }
        throw new ComponentNotFoundException(name + " not found");
    }

    public ZScript getEventHandler(String evtnm) {
        final EventHandler evthd = _auxinf != null && _auxinf.evthds != null ? _auxinf.evthds.get(this, evtnm) : null;
        return evthd != null ? evthd.getZScript() : null;
    }

    public void addSharedEventHandlerMap(EventHandlerMap evthds) {
        if (evthds != null && !evthds.isEmpty()) {
            unshareEventHandlerMap(false);
            if (initAuxInfo().evthds == null) {
                _auxinf.evthds = evthds;
                _auxinf.evthdsShared = true;
            } else {
                _auxinf.evthds.addAll(evthds);
            }

            final Desktop desktop = getDesktop();
            if (desktop != null)
                onListenerChange(desktop, true);
        }
    }

    public Set<String> getEventHandlerNames() {
        if (_auxinf != null && _auxinf.evthds != null)
            return _auxinf.evthds.getEventNames();
        return Collections.emptySet();
    }

    private void onListenerChange(Desktop desktop, boolean listen) {
        if (listen) {
            if (Events.isListened(this, Events.ON_CLIENT_INFO, false)) { //asap+deferrable
                response(new AuClientInfo(desktop));
                getDesktop().setAttribute("org.zkoss.desktop.clientinfo.enabled", true);
                //We always fire event not a root, since we don't like to
                //check when setParent or setPage is called
            }
            if (Events.isListened(this, Events.ON_PIGGYBACK, false))
                ((DesktopCtrl) desktop).onPiggybackListened(this, true);
            if (Events.isListened(this, Events.ON_VISIBILITY_CHANGE, false))
                getDesktop().setAttribute("org.zkoss.desktop.visibilitychange.enabled", true);
        } else {
            if (!Events.isListened(this, Events.ON_PIGGYBACK, false))
                ((DesktopCtrl) desktop).onPiggybackListened(this, false);
        }
    }

    public void addEventHandler(String name, EventHandler evthd) {
        if (name == null || evthd == null)
            throw new IllegalArgumentException("name and evthd required");

        unshareEventHandlerMap(true);
        _auxinf.evthds.add(name, evthd);
    }

    /** Clones the shared event handlers, if shared.
     * @param autocreate whether to create an event handler map if not available.
     */
    private void unshareEventHandlerMap(boolean autocreate) {
        if (_auxinf != null && _auxinf.evthdsShared) {
            _auxinf.evthds = (EventHandlerMap) _auxinf.evthds.clone();
            _auxinf.evthdsShared = false;
        } else if (autocreate && initAuxInfo().evthds == null) {
            _auxinf.evthds = new EventHandlerMap();
        }
    }

    public Annotation getAnnotation(String propName, String annotName) {
        return _auxinf != null && _auxinf.annots != null ? _auxinf.annots.getAnnotation(propName, annotName) : null;
    }

    public Collection<Annotation> getAnnotations(String propName, String annotName) {
        if (_auxinf != null && _auxinf.annots != null)
            return _auxinf.annots.getAnnotations(propName, annotName);
        return Collections.emptyList();
    }
    public Collection<Annotation> getAnnotations(String propName) {
        if (_auxinf != null && _auxinf.annots != null)
            return _auxinf.annots.getAnnotations(propName);
        return Collections.emptyList();
    }

    public List<String> getAnnotatedPropertiesBy(String annotName) {
        if (_auxinf != null && _auxinf.annots != null)
            return _auxinf.annots.getAnnotatedPropertiesBy(annotName);
        return Collections.emptyList();
    }

    public List<String> getAnnotatedProperties() {
        if (_auxinf != null && _auxinf.annots != null)
            return _auxinf.annots.getAnnotatedProperties();
        return Collections.emptyList();
    }

    /** Add a map of annotations which is shared by other components.
     * In other words, this component shall have all annotations
     * defined in the specified map, annots. Meanwhile, this component
     * shall not modify annots, since it is shared.
     * The caller shall not change annots after the invocation, too
     */
    private void addSharedAnnotationMap(AnnotationMap annots) {
        if (annots != null && !annots.isEmpty()) {
            unshareAnnotationMap(false);
            if (initAuxInfo().annots == null) {
                _auxinf.annots = annots;
                _auxinf.annotsShared = true;
            } else {
                _auxinf.annots.addAll(annots);
            }
        }
    }
    public void addAnnotation(String propName, String annotName, Map<String, String[]> annotAttrs) {
        unshareAnnotationMap(true);
        _auxinf.annots.addAnnotation(propName, annotName, fixAttrValues(annotAttrs), null);
    }

    /** Used to resolve the backward compatibility:
     * ZK 6 expects String[], but ZK 5 might pass String as the value.
     * <p>For better performance, we don't put this method to AnnotationMap.
     * After all, ComponentInfo/ComponentDefinition.addAnnotation not for app
     */
    private Map<String, String[]> fixAttrValues(Map<?, ?> attrs) {
        if (attrs == null)
            return null;

        for (Map.Entry<?, ?> m0 : attrs.entrySet()) {
            Object key = m0.getKey();
            Object val = m0.getValue();
            if ((key != null && !(key instanceof String)) || (val != null && !(val instanceof String[]))) { //need to convert
                final Map<String, String[]> as = new LinkedHashMap<String, String[]>(4);
                for (Map.Entry<?, ?> me : attrs.entrySet()) {
                    key = me.getKey();
                    if (key != null && !(key instanceof String))
                        throw new UiException("Illegal attribute name, " + key);

                    val = me.getValue();
                    if (val == null || val instanceof String[])
                        as.put((String) key, (String[]) val);
                    else if (val instanceof String)
                        as.put((String) key, new String[] { (String) val });
                    else
                        throw new UiException("Illegagl attribute value, " + val);
                }
                return as;
            }
        }
        return cast(attrs);
    }

    /** Clones the shared annotations, if shared.
     * @param autocreate whether to create an annotation map if not available.
     */
    private void unshareAnnotationMap(boolean autocreate) {
        if (_auxinf != null && _auxinf.annotsShared) {
            _auxinf.annots = (AnnotationMap) _auxinf.annots.clone();
            _auxinf.annotsShared = false;
        } else if (autocreate && initAuxInfo().annots == null) {
            _auxinf.annots = new AnnotationMap();
        }
    }

    public void sessionWillPassivate(Page page) {
        Set<Object> uniqueAttrs = new LinkedHashSet<Object>(); //ZK-2701: use set to record unique attrs and listeners
        if (_auxinf != null && _auxinf.attrs != null) {
            uniqueAttrs.addAll(_auxinf.attrs.getAttributes().values());
            willPassivate(uniqueAttrs); //ZK-2701: only passivate the same object once
            willPassivate(_auxinf.attrs.getListeners());
        }

        if (_auxinf != null && _auxinf.listeners != null)
            for (Iterator<List<EventListenerInfo>> it = CollectionsX
                    .comodifiableIterator(_auxinf.listeners.values()); it.hasNext();)
                for (EventListenerInfo li : it.next()) {
                    if (uniqueAttrs.add(li.listener)) //ZK-2701: only passivate the same object once
                        willPassivate(li.listener);
                }

        for (AbstractComponent p = (AbstractComponent) getFirstChild(); p != null; p = p._next)
            p.sessionWillPassivate(page); //recursive
    }

    public void sessionDidActivate(Page page) {
        _page = page;

        Set<Object> uniqueAttrs = new LinkedHashSet<Object>(); //ZK-2701: use set to record unique attrs and listeners
        if (_auxinf != null && _auxinf.attrs != null) {
            uniqueAttrs.addAll(_auxinf.attrs.getAttributes().values());
            didActivate(uniqueAttrs); //ZK-2701: only activate the same object once
            didActivate(_auxinf.attrs.getListeners());
            if (_parent == null)
                _auxinf.attrs.notifyParentChanged(_page);
        }

        if (_auxinf != null && _auxinf.listeners != null)
            for (Iterator<List<EventListenerInfo>> it = CollectionsX
                    .comodifiableIterator(_auxinf.listeners.values()); it.hasNext();)
                //ZK-3224: Prevent ConcurrentModificationException
                for (Iterator<EventListenerInfo> itli = CollectionsX.comodifiableIterator(it.next()); itli.hasNext();) {
                    EventListenerInfo li = itli.next();
                    if (uniqueAttrs.add(li.listener)) //ZK-2701: only activate the same object once
                        didActivate(li.listener);
                }

        for (AbstractComponent p = (AbstractComponent) getFirstChild(); p != null; p = p._next)
            p.sessionDidActivate(page); //recursive
    }

    /** Utility to invoke {@link ComponentActivationListener#willPassivate}
     * for each object in the collection.
     * @param c a collection of objects. Ignored if null.
     * @since 3.6.4
     */
    @SuppressWarnings("unchecked")
    protected void willPassivate(Collection<?> c) {
        if (c != null)
            for (Iterator it = new ArrayList(c).iterator(); it.hasNext();)
                willPassivate(it.next());
    }

    /** Utility to invoke {@link ComponentActivationListener#willPassivate}
     * for the specified object.
     * @param o the object to invoke. Ignore if
     * ComponentActivationListener not implemented or null.
     * @since 3.6.4
     */
    protected void willPassivate(Object o) {
        if (o instanceof ComponentActivationListener)
            ((ComponentActivationListener) o).willPassivate(this);
    }

    /** Utility to invoke {@link ComponentActivationListener#didActivate}
     * for each object in the collection.
     * @param c a collection of objects. Ignored if null.
     * @since 3.6.4
     */
    @SuppressWarnings("unchecked")
    protected void didActivate(Collection<?> c) {
        if (c != null)
            for (Iterator it = new ArrayList(c).iterator(); it.hasNext();)
                didActivate(it.next());
    }

    /** Utility to invoke {@link ComponentActivationListener#didActivate}
     * for the specified object.
     * @param o the object to invoke. Ignore if
     * ComponentActivationListener not implemented or null.
     * @since 3.6.4
     */
    protected void didActivate(Object o) {
        if (o instanceof ComponentActivationListener)
            ((ComponentActivationListener) o).didActivate(this);
    }

    /** Returns the extra controls that tell ZK how to handle this component
     * specially.
     * It is used only by component developers.
     *
     * <p>Default: null.
     *
     * @see ComponentCtrl#getExtraCtrl
     */
    public Object getExtraCtrl() {
        return null;
    }

    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(3);

    static {
        _properties.put("id", new StringPropertyAccess() {
            public void setValue(Component cmp, String id) {
                cmp.setId(id);
            }

            public String getValue(Component cmp) {
                return cmp.getId();
            }
        });
        _properties.put("mold", new StringPropertyAccess() {
            public void setValue(Component cmp, String mold) {
                cmp.setMold(mold);
            }

            public String getValue(Component cmp) {
                return cmp.getMold();
            }
        });
        _properties.put("visible", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean visible) {
                cmp.setVisible(visible);
            }

            public Boolean getValue(Component cmp) {
                return cmp.isVisible();
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        return _properties.get(prop);
    }

    /** Notifies that an {@link WrongValueException} instance is thrown,
     * and {@link WrongValueException#getComponent} is this component.
     * It is a callback and the component can store the error message,
     * show up the custom information, or even 'eat' the exception.
     *
     * <p>Default: does nothing but returns ex.
     *
     * @param ex the exception being thrown (never null)
     * @return the exception to throw, or null to ignore the exception
     * In most cases, just return ex
     * @since 2.4.0
     */
    public WrongValueException onWrongValue(WrongValueException ex) {
        return ex;
    }

    public AuService getAuService() {
        return _auxinf != null ? _auxinf.ausvc : null;
    }

    public void setAuService(AuService ausvc) {
        if (ausvc != null)
            initAuxInfo().ausvc = ausvc;
        else if (_auxinf != null)
            _auxinf.ausvc = null;
    }

    /** Handles an AU request. It is invoked internally.
     *
     * <p>Default: it handles echo and setAttr, and it converts other request
     * to an event (by {@link Event#getEvent}) and then posts the event
     * (by {@link Events#postEvent}).
     *
     * <p>Application developer can plug the custom service to handle
     * the AU request by {@link #setAuService}.
     * @since 5.0.0
     * @see #setAuService
     */
    public void service(AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if ("echo".equals(cmd)) {
            final List data2 = (List) request.getData().get("");
            Events.postEvent(new Event((String) data2.get(0), this,
                    data2.size() > 1 ? AuEcho.getData(this, data2.get(1)) : null));
        } else if ("setAttr".equals(cmd)) {
            final List data2 = (List) request.getData().get("");
            updateByClient((String) data2.get(0), data2.get(1));
        } else
            Events.postEvent(Event.getEvent(request));
    }

    public void service(Event event, Scope scope) throws Exception {
        final String evtnm = event.getName();
        final Method mtd = ComponentsCtrl.getEventMethod(getClass(), evtnm);
        if (_auxinf != null)
            service(event, scope, _auxinf.listeners != null ? _auxinf.listeners.get(evtnm) : null,
                    _auxinf.evthds != null ? _auxinf.evthds.get(this, evtnm) : null, mtd, false);
        else
            service(event, scope, null, null, mtd, false);
    }

    /**
     * @param skipPageListener whether to skip page's event listener.
     */
    /*package*/ void service(Event event, Scope scope, final List<EventListenerInfo> listeners,
            final EventHandler evthd, final Method mtd, boolean skipPageListener) throws Exception {
        final Execution exec = Executions.getCurrent();
        final Desktop desktop = exec.getDesktop();
        final Page page = _page != null ? _page : desktop.getFirstPage();
        if (page == null || !page.isAlive()) {
            String msg = (page == null ? "No page is available in " + desktop : "Page " + page + " was destroyed");
            if (desktop.isAlive())
                msg += " (but desktop is alive)";
            else
                msg += " because desktop was destroyed.\n"
                        + "It is usually caused by invalidating the native session directly. "
                        + "If it is required, please set Attributes.RENEW_NATIVE_SESSION first.";
            log.warn(msg);
        }

        //EventListener always is called even if comp isn't attached (e.g., EventQueue)
        final ExecInfo execinf;
        ((ExecutionCtrl) exec).setExecutionInfo(execinf = new ExecInfo(event));
        if (listeners != null)
            for (Iterator<EventListenerInfo> it = new LinkedList<EventListenerInfo>(listeners).iterator(); it
                    .hasNext();) {
                final EventListenerInfo li = it.next();
                if (li.priority < 1000)
                    break; //no more

                execinf.update(null, li.listener, null);
                onEvent(li.listener, event);
                if (!event.isPropagatable())
                    return; //done
            }

        //ZScript called only if comp attached to an active page
        if (page != null && getDesktop() != null) { //this.desktop, not exec.desktop
            final ZScript zscript = evthd != null ? evthd.getZScript() : null;
            if (zscript != null) {
                execinf.update(null, null, zscript);
                page.interpret(zscript.getLanguage(), zscript.getContent(page, this), scope);
                if (!event.isPropagatable())
                    return; //done
            }
        }

        if (listeners != null)
            for (Iterator<EventListenerInfo> it = new LinkedList<EventListenerInfo>(listeners).iterator(); it
                    .hasNext();) {
                final EventListenerInfo li = it.next();
                if (li.priority < 1000) {
                    execinf.update(null, li.listener, null);
                    onEvent(li.listener, event);
                    if (!event.isPropagatable())
                        return; //done
                }
            }

        //Like EventListener, method is always called
        if (mtd != null) {
            execinf.update(mtd, null, null);

            if (mtd.getParameterTypes().length == 0)
                mtd.invoke(this, (Object[]) null);
            else
                mtd.invoke(this, new Object[] { event });
            if (!event.isPropagatable())
                return; //done
        }

        if (!skipPageListener && page != null)
            for (EventListener<? extends Event> el : page.getEventListeners(event.getName())) {
                //Note: CollectionsX.comodifiableIterator is used so OK to iterate
                execinf.update(null, el, null);
                onEvent(el, event);
                if (!event.isPropagatable())
                    return; //done
            }
    }

    @SuppressWarnings("unchecked")
    private static void onEvent(EventListener listener, Event event) throws Exception {
        listener.onEvent(event);
    }

    public EventListenerMap getEventListenerMap() {
        return new EventListenerMapImpl(_auxinf != null ? _auxinf.listeners : null,
                _auxinf != null ? _auxinf.evthds : null);
    }

    /** Called when the widget running at the client asks the server
     * to update a value. The update is caused by an AU request named <code>setAttr</code>
     * (by invoking zk.Widget's smartUpdate at client).
     *
     * <p>By default, it does nothing but log a warning message, since
     * it is not safe to allow the client to update a field arbitrary.
     * <p>However, if you'd like to allow the update for a particular component
     * you could do one of the following
     * <ol>
     * <li>For component developers: override this method to update the field
     * directly. For example,<br/>
    <pre><code>protected void updateByClient(String name, Object value) {
    if ("disabled".equals(name))
        setDisabled(name, ((Boolean)value).booleanValue());
    else
        super.updateByClient(name, value);</code></pre></li>
     * <li>For application developers: set an attribute called
     * <code>org.zkoss.zk.ui.updateByClient</code> to be true.
     * Then, this method will use reflection to find out the setter to update
     * the value. Nothing happens if the method is not found.</li>
     *
     * <p>Notice: this method will invoke {@link #disableClientUpdate} to
     * disable any update to the client, when calling the setter.
     *
     * <p>If you want to enable the client update for all instances of a given
     * component (though not recommended for the security reason), you could
     * refer to <a href="http://books.zkoss.org/wiki/Small_Talks/2011/May/New_Features_of_ZK_5.0.7#Client-side_smartUpdate_now_disabled_by_default">here</a>.
     *
     * <p>See also <a href="http://www.zkoss.org/javadoc/latest/jsdoc/zk/Widget.html#smartUpdate%28_global_.String,%20zk.Object,%20int%29">zk.Widget.smartUpdate()</a>.
     * @since 5.0.0
     */
    protected void updateByClient(String name, Object value) {
        Object o = getAttribute("org.zkoss.zk.ui.updateByClient");
        if (!(o instanceof Boolean && ((Boolean) o).booleanValue()) && !(o instanceof String && "true".equals(o))) {
            log.warn("Ignore update of " + name + "=" + value + " from client for " + this.getClass());
            return; //ignored
        }

        Method m;
        final String mtdnm = Classes.toMethodName(name, "set");
        Object[] args = new Object[] { value };
        try {
            m = Classes.getMethodByObject(getClass(), mtdnm, args);
        } catch (NoSuchMethodException ex) {
            try {
                m = Classes.getCloseMethod(getClass(), mtdnm, new Class[] { String.class });
            } catch (NoSuchMethodException e2) {
                try {
                    m = Classes.getCloseMethod(getClass(), mtdnm, new Class[] { null });
                } catch (NoSuchMethodException e3) {
                    log.warn("setter not found", ex);
                    return; //ignore it
                }
            }
            try {
                args[0] = Classes.coerce(m.getParameterTypes()[0], value);
            } catch (Throwable e2) {
                log.warn(m + " requires " + m.getParameterTypes()[0] + ", not " + value);
                return; //ignore it
            }
        }

        disableClientUpdate(true);
        try {
            m.invoke(this, args);
        } catch (Throwable ex) {
            throw UiException.Aide.wrap(ex);
        } finally {
            disableClientUpdate(false);
        }
    }

    //-- Object --//
    public String toString() {
        final String clsnm = getClass().getName();
        final int j = clsnm.lastIndexOf('.');
        return "<" + clsnm.substring(j + 1) + ' ' + _uuid + (Strings.isEmpty(_id) ? "" : "#" + _id) + '>';
    }

    /** Holds info shared of the same ID space. */
    private class SpaceInfo {
        /** A map of ((String id, Component fellow). */
        private Map<String, Component> fellows = new HashMap<String, Component>(32);
    }

    private class ChildIter implements ListIterator<Component> {
        private AbstractComponent _p, _lastRet;
        private int _j;
        private int _modCntSnap;

        private ChildIter(int index) {
            int nChild;
            if (index < 0 || index > (nChild = nChild()))
                throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + nChild());

            if (index < (nChild >> 1)) {
                _p = _chdinf.first;
                for (_j = 0; _j < index; _j++)
                    _p = _p._next;
            } else {
                _p = null; //means the end of the list
                for (_j = nChild; _j > index; _j--)
                    _p = _p != null ? _p._prev : _chdinf.last;
            }

            _modCntSnap = modCntChd();
        }

        public boolean hasNext() {
            return _j < nChild();
        }

        public Component next() {
            if (_j >= nChild())
                throw new java.util.NoSuchElementException();
            checkComodification();

            _lastRet = _p;
            _p = _p._next;
            _j++;
            return _lastRet;
        }

        public boolean hasPrevious() {
            return _j > 0;
        }

        public Component previous() {
            if (_j <= 0)
                throw new java.util.NoSuchElementException();
            checkComodification();

            _lastRet = _p = _p != null ? _p._prev : _chdinf.last;
            _j--;
            return _lastRet;
        }

        private void checkComodification() {
            if (modCntChd() != _modCntSnap)
                throw new java.util.ConcurrentModificationException();
        }

        public int nextIndex() {
            return _j;
        }

        public int previousIndex() {
            return _j - 1;
        }

        public void add(Component newChild) {
            if (newChild.getParent() == AbstractComponent.this)
                throw new UnsupportedOperationException("Unable to add component with the same parent: " + newChild);
            //1. it is confusing to allow adding (with replace)
            //2. the code is sophisticated
            checkComodification();

            insertBefore(newChild, _p);
            ++_j;
            _lastRet = null;
            //spec: cause remove to throw ex if no next/previous
            ++_modCntSnap;
            //don't assign from modCntChd directly since deriving class
            //might manipulate others in insertBefore
        }

        public void remove() {
            if (_lastRet == null)
                throw new IllegalStateException();
            checkComodification();

            if (_p == _lastRet)
                _p = _lastRet._next; //previous was called
            else
                --_j; //next was called

            removeChild(_lastRet);

            _lastRet = null;
            ++_modCntSnap;
        }

        public void set(Component o) {
            throw new UnsupportedOperationException();
            //Possible to implement this but confusing to developers
            //if o has the same parent (since we have to move)
        }
    }

    //Cloneable//
    public Object clone() {
        final AbstractComponent clone;
        try {
            clone = (AbstractComponent) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }

        //1. make it not belonging to any page
        clone._page = null;
        clone._parent = null;

        //2. clone AuxInfo
        if (_auxinf != null)
            clone._auxinf = (AuxInfo) clone._auxinf.clone();

        //3. clone children (deep cloning)
        if (_chdinf != null) {
            clone._chdinf = _chdinf.clone(clone);

            //child's attrs's notification
            for (AbstractComponent p = clone._chdinf.first; p != null; p = p._next)
                if (p._auxinf != null && p._auxinf.attrs != null)
                    p._auxinf.attrs.notifyParentChanged(clone);
        }

        //4. init AuxInfo
        if (_auxinf != null)
            _auxinf.initClone(clone, clone._auxinf);

        return clone;
    }

    private void cloneSpaceInfoFrom(SpaceInfo from) {
        //rebuild ID space by binding itself and all children
        if (!isAutoId(_id))
            this.bindToIdSpace(this);
        for (AbstractComponent p = (AbstractComponent) getFirstChild(); p != null; p = p._next)
            addToIdSpacesDown(p, (IdSpace) this);
    }

    //Serializable//
    //NOTE: they must be declared as private
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        //No need to unshare annots and evthds, since stored as an independent copy

        s.defaultWriteObject();

        //write definition
        if (_def == ComponentsCtrl.DUMMY) {
            s.writeObject(null);
        } else {
            LanguageDefinition langdef = _def.getLanguageDefinition();
            if (langdef != null) {
                s.writeObject(langdef.getName());
                s.writeObject(_def.getName());
            } else if (_def instanceof Serializable) {
                Serializable def = (Serializable) _def;
                s.writeObject(def);
            } else {
                throw new NotSerializableException(_def.getClass().getName());
            }
        }

        //write children
        for (AbstractComponent p = (AbstractComponent) getFirstChild(); p != null; p = p._next)
            s.writeObject(p);
        s.writeObject(null);

        //write auxinf if necessary
        if (_auxinf == null)
            return;

        //write attrs
        if (_auxinf.attrs != null) {
            final Map<String, Object> attrmap = _auxinf.attrs.getAttributes();
            willSerialize(attrmap.values());
            final List<ScopeListener> attrlns = _auxinf.attrs.getListeners();
            willSerialize(attrlns);

            Serializables.smartWrite(s, attrmap);
            Serializables.smartWrite(s, attrlns);
        } else {
            Serializables.smartWrite(s, (Map<String, Object>) null);
            Serializables.smartWrite(s, (List<ScopeListener>) null);
        }

        EventListenerInfo.write(s, this, _auxinf.listeners);

        willSerialize(_auxinf.ausvc);
        Serializables.smartWrite(s, _auxinf.ausvc);
    }

    /** Utility to invoke {@link ComponentSerializationListener#willSerialize}
     * for each object in the collection.
     * @param c a collection of objects. Ignored if null.
     * @since 3.6.4
     */
    protected void willSerialize(Collection c) {
        if (c != null)
            for (Iterator it = c.iterator(); it.hasNext();)
                willSerialize(it.next());
    }

    /** Utility to invoke {@link ComponentSerializationListener#willSerialize}
     * for the specified object.
     * @param o the object to invoke. Ignore if
     * ComponentSerializationListener not implemented or null.
     * @since 3.6.4
     */
    protected void willSerialize(Object o) {
        if (o instanceof ComponentSerializationListener)
            ((ComponentSerializationListener) o).willSerialize(this);
    }

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

        //read definition
        Object def = s.readObject();
        if (def instanceof String) {
            LanguageDefinition langdef = null;
            try {
                langdef = LanguageDefinition.lookup((String) def);
            } catch (DefinitionNotFoundException ex) {
                // expected
            }
            if (langdef != null) {
                _def = langdef.getComponentDefinitionIfAny((String) s.readObject());
                //don't throw exception since some might not be associated
                //with a definition (e.g., JSP's native and page components)
            } else {
                s.readObject(); //ignore the component name
                _def = null;
            }
        } else {
            _def = (ComponentDefinition) def;
        }
        if (_def == null)
            _def = ComponentsCtrl.DUMMY;

        //read children
        for (AbstractComponent q = null;;) {
            final AbstractComponent child = (AbstractComponent) s.readObject();
            if (child == null) {
                if (_chdinf != null)
                    _chdinf.last = q;
                break; //no more
            }
            ++initChildInfo().nChild;
            if (q != null)
                q._next = child;
            else
                _chdinf.first = child;
            child._prev = q;
            child._parent = this;
            q = child;
        }

        //Read auxinf
        if (_auxinf == null)
            return;

        //read attrs
        attrs();
        final Map<String, Object> attrmap = _auxinf.attrs.getAttributes();
        Serializables.smartRead(s, attrmap);
        final List<ScopeListener> attrlns = _auxinf.attrs.getListeners();
        Serializables.smartRead(s, attrlns);
        if (attrmap.isEmpty() && attrlns.isEmpty())
            _auxinf.attrs = null;
        else if (_parent != null)
            _auxinf.attrs.notifyParentChanged(_parent);

        _auxinf.listeners = EventListenerInfo.read(s, this);

        //restore _auxinf.spaceInfo
        if (this instanceof IdSpace) {
            _auxinf.spaceInfo = new SpaceInfo();

            //restore ID space by binding itself and all children
            if (!isAutoId(_id))
                bindToIdSpace(this);
            for (AbstractComponent ac = (AbstractComponent) getFirstChild(); ac != null; ac = ac._next)
                addToIdSpacesDown(ac, (IdSpace) this);
        }

        //didDeserialize
        didDeserialize(attrmap.values());
        didDeserialize(attrlns);
        didDeserialize(_auxinf.ausvc = (AuService) s.readObject());
    }

    /** Utility to invoke {@link ComponentSerializationListener#didDeserialize}
     * for each object in the collection.
     * @param c a collection of objects. Ignored if null.
     * @since 3.6.4
     */
    protected void didDeserialize(Collection c) {
        if (c != null)
            for (Iterator it = c.iterator(); it.hasNext();)
                didDeserialize(it.next());
    }

    /** Utility to invoke {@link ComponentSerializationListener#didDeserialize}
     * for the specified object.
     * @param o the object to invoke. Ignore if
     * ComponentSerializationListener not implemented or null.
     * @since 3.6.4
     */
    protected void didDeserialize(Object o) {
        if (o instanceof ComponentSerializationListener)
            ((ComponentSerializationListener) o).didDeserialize(this);
    }

    /** Used to forward events (for the forward conditions).
     */
    private class ForwardListener implements EventListener<Event>, ComponentCloneListener, java.io.Serializable {
        //Note: it is not serializable since it is handled by
        //AbstractComponent.writeObject

        private final String _orgEvent;

        private ForwardListener(String orgEvent) {
            _orgEvent = orgEvent;
        }

        public void onEvent(Event event) {
            final ForwardInfo info = _auxinf.forwards.get(_orgEvent);
            if (info != null)
                for (TargetInfo ti : new ArrayList<TargetInfo>(info.targets)) {
                    Component target = resolveForwardTarget(ti.target);
                    if (target == null) {
                        final IdSpace owner = getSpaceOwner();
                        if (owner instanceof Component) {
                            target = (Component) owner;
                        } else {
                            //Use the root component instead
                            for (target = AbstractComponent.this;;) {
                                final Component p = target.getParent();
                                if (p == null)
                                    break;
                                target = p;
                            }
                        }
                    }

                    //bug #2790393 Forward event listener shall be called immediately
                    //(since 3.6.2) change from postEvent to sendEvent to
                    //make forward event deterministic
                    Events.sendEvent(new ForwardEvent(ti.event, target, event, ti.data));
                }
        }

        //ComponentCloneListener//
        public Object willClone(Component comp) {
            return null; //handle by AbstractComponent.clone
        }
    }

    private Component resolveForwardTarget(Object target) {
        return target instanceof String ? Components.pathToComponent((String) target, this) : (Component) target;
    }

    /** Returns the default mold for the given class.
     * <p>Default: check the library property called xxx.mold, where xxx is
     * the name of the give class. If not found or empty, "default" is assumed.
     * <p>Subclass might override this method to use the default mold of the base
     * class, such as
     * <pre><code>
     *protected String getDefaultMold(Class klass) {
     *   return super.getDefaultMold(Button.class);
     *}</code></pre>
     * @since 5.0.3
     */
    protected String getDefaultMold(Class<? extends Component> klass) {
        return Impls.defaultMold(klass);
    }

    private final AuxInfo initAuxInfo() {
        if (_auxinf == null)
            _auxinf = new AuxInfo();
        return _auxinf;
    }

    public Template getTemplate(String name) {
        return _auxinf != null && _auxinf.templates != null ? _auxinf.templates.get(name) : null;
    }

    public Template setTemplate(String name, Template template) {
        if (template == null) {
            return _auxinf != null && _auxinf.templates != null ? _auxinf.templates.remove(name) : null;
        } else {
            AuxInfo auxinf = initAuxInfo();
            if (auxinf.templates == null)
                auxinf.templates = new HashMap<String, Template>(4);
            return auxinf.templates.put(name, template);
        }
    }

    public Set<String> getTemplateNames() {
        if (_auxinf != null && _auxinf.templates != null)
            return _auxinf.templates.keySet();
        return Collections.emptySet();
    }

    public Component query(String selector) {
        final Iterator<Component> found = Selectors.iterable(this, selector).iterator();
        return found.hasNext() ? found.next() : null;
    }

    public Iterable<Component> queryAll(String selector) {
        return Selectors.iterable(this, selector);
    }

    /** Merge multiple members into an single object (and create on demand)
     * to minimize the footprint
     * @since 5.0.4
     */
    private static class AuxInfo implements java.io.Serializable, Cloneable {
        /** shadow roots since 8.0.0 */
        private List<ShadowElement> seRoots;
        /** The mold. */
        private String mold;

        /** The info of the ID space, or null if IdSpace is NOT implemented. */
        private transient SpaceInfo spaceInfo;
        /** Component attributes. */
        private transient SimpleScope attrs;
        /** A map of event listener: Map(evtnm, List(EventListenerInfo)). */
        private transient Map<String, List<EventListenerInfo>> listeners;

        /** A map of annotations. Serializable since a component might have
         * its own annotations.
         */
        private AnnotationMap annots;
        /** A map of event handler to handle events. */
        private EventHandlerMap evthds;
        /** A map of forward conditions:
         * Map(String orgEvt, ForwardInfo fi>).
         */
        private Map<String, ForwardInfo> forwards;

        /** the Au service. */
        private transient AuService ausvc;

        /** The widget class. */
        private String wgtcls;
        /** A map of client event handlers, Map(String evtnm, String script). */
        private Map<String, String> wgtlsns;
        /** A map of client properties to override, Map(String name, String script). */
        private Map<String, String> wgtovds;
        /** A map of client DOM attributes to set, Map(String name, String value). */
        private Map<String, String> domattrs;
        /** A map of component lifecycle callback to set. Since 8.0.0  */
        private Map<String, List<Callback<?>>> callbacks;
        /** The AU tag. */
        private String autag;

        /** The templates. */
        private Map<String, Template> templates;

        /** Whether this component is stub-only (0: inherit, -1: false, 1: true). */
        private byte stubonly;

        /** Whether annots is shared with other components. */
        private transient boolean annotsShared;
        /** Whether evthds is shared with other components. */
        private transient boolean evthdsShared;
        /** Whether this component is visible. */
        private boolean visible = true;
        //F80 - store subtree's binder annotation count
        private boolean hasBindingAnnot = false;
        private int subAnnotCnt = 0;

        private boolean hasBindingAnnotInfo = false;

        public Object clone() {
            final AuxInfo clone;
            try {
                clone = (AuxInfo) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new InternalError();
            }
            if (wgtlsns != null)
                clone.wgtlsns = new LinkedHashMap<String, String>(wgtlsns);
            if (wgtovds != null)
                clone.wgtovds = new LinkedHashMap<String, String>(wgtovds);
            if (domattrs != null)
                clone.domattrs = new LinkedHashMap<String, String>(domattrs);
            if (callbacks != null)
                clone.callbacks = new LinkedHashMap<String, List<Callback<?>>>(callbacks);

            //clone annotation and event handlers
            if (!annotsShared && annots != null)
                clone.annots = (AnnotationMap) annots.clone();
            if (!evthdsShared && evthds != null)
                clone.evthds = (EventHandlerMap) evthds.clone();
            return clone;
        }

        /** Clone for the stub component ({@link #replaceWith(AbstractComponent, boolean, boolean, boolean)}). */
        private AuxInfo cloneStub(AbstractComponent owner, boolean bListener) {
            //No need to clone visible since it is meaningless in StubComponent
            if (bListener && (evthds != null || listeners != null)) {
                //No need to make a copy of  evthds and listeners since Stub replaced the old one
                final AuxInfo clone = new AuxInfo();
                clone.evthdsShared = evthdsShared;
                clone.evthds = evthds;
                clone.listeners = listeners;
                return clone;
            }
            return null;
        }

        /** 2nd phase of clone (after children are cloned). */
        private void initClone(AbstractComponent owner, AuxInfo clone) {
            //spaceinfo (after children is cloned)
            if (spaceInfo != null) {
                clone.spaceInfo = owner.new SpaceInfo();
                owner.cloneSpaceInfoFrom(spaceInfo);
            }

            //clone attrs
            if (attrs != null)
                clone.attrs = attrs.clone(owner);

            //clone listener
            cloneListeners(owner, clone);

            //clone forwards (after children is cloned)
            if (forwards != null) {
                clone.forwards = null;
                for (Map.Entry<String, ForwardInfo> me : forwards.entrySet()) {
                    final String orgEvent = me.getKey();
                    final ForwardInfo info = me.getValue();
                    final List<TargetInfo> tis = info.targets;
                    for (TargetInfo ti : tis)
                        owner.addForward0(orgEvent, ti.target, ti.event, ti.data);
                }
            }

            if (seRoots != null) {
                clone.seRoots = seRoots.stream()
                        .map(shadowElement -> (ShadowElement) ((Component) shadowElement).clone())
                        .collect(Collectors.toList());
                clone.seRoots.stream()
                        .filter(shadowElement -> shadowElement instanceof HtmlShadowElement)
                        .forEach(shadowElement -> ((HtmlShadowElement) shadowElement).initClone(owner));
            }

            //AuService
            if (ausvc instanceof ComponentCloneListener)
                clone.ausvc = (AuService) ((ComponentCloneListener) ausvc).willClone(owner);
        }

        private void cloneListeners(AbstractComponent owner, AuxInfo clone) {
            if (listeners != null) {
                clone.listeners = new HashMap<String, List<EventListenerInfo>>(4);
                for (Map.Entry<String, List<EventListenerInfo>> me : listeners.entrySet()) {
                    final List<EventListenerInfo> list = new LinkedList<EventListenerInfo>();
                    for (EventListenerInfo li : me.getValue()) {
                        if (li.listener instanceof ComponentCloneListener) {
                            Object val = ((ComponentCloneListener) li.listener).willClone(owner);
                            if (val == null)
                                continue; //don't use it in clone
                            li = new EventListenerInfo(li.priority, (EventListener<? extends Event>) val);
                        }
                        list.add(li);
                    }
                    if (!list.isEmpty())
                        clone.listeners.put(me.getKey(), list);
                }
            }
        }

        private void render(ContentRenderer renderer) throws IOException {
            renderer.renderWidgetListeners(wgtlsns);
            renderer.renderWidgetOverrides(wgtovds);
            renderer.renderClientAttributes(domattrs);
        }
    }

    private final ChildInfo initChildInfo() {
        if (_chdinf == null)
            _chdinf = new ChildInfo();
        return _chdinf;
    }

    /*package*/ static class ChildInfo implements Cloneable/* not java.io.Serializable*/ {
        /** The first child. */
        /*package*/ AbstractComponent first;
        /** The last child. */
        /*package*/ AbstractComponent last;
        /** # of children. */
        /*package*/ int nChild;
        /** Set of components that are being added or removed.
         * _aring[0]: add, _aring[1]: remove
         * It is used to prevent dead-loop between {@link #removeChild}
         * and {@link #setParent}.
         */
        private Set<Component>[] _aring; //use an array to save memory
        /** The modification count used to avoid co-modification of _next, _prev..
         */
        /*package*/ int modCntChd;
        /** The virtual ID space used when this component is a root component,
         * but not attached to a page, nor implement IdSpace.
         */
        private IdSpace vispace;

        private ChildInfo() {
        }

        public Object clone() {
            try {
                final Object o = super.clone();
                ((ChildInfo) o).vispace = null; //clean up
                return o;
            } catch (CloneNotSupportedException e) {
                throw new InternalError();
            }
        }

        private ChildInfo clone(AbstractComponent owner) {
            final ChildInfo clone = (ChildInfo) clone();

            AbstractComponent q = null;
            for (AbstractComponent p = first; p != null; p = p._next) {
                AbstractComponent child = (AbstractComponent) p.clone();
                if (q != null)
                    q._next = child;
                else
                    clone.first = child;
                child._prev = q;
                q = child;

                child._parent = owner; //correct it
            }
            clone.last = q;
            return clone;
        }

        /** Returns whether the child is being removed.
         */
        private boolean inRemoving(Component child) {
            return _aring != null && _aring[1] != null && _aring[1].contains(child);
        }

        /** Sets if the child is being removed.
         */
        private void markRemoving(Component child, boolean set) {
            markARing(child, set, 1);
        }

        /** Returns whether the child is being added.
         */
        private boolean inAdding(Component child) {
            return _aring != null && _aring[0] != null && _aring[0].contains(child);
        }

        /** Sets if the child is being added.
         */
        private void markAdding(Component child, boolean set) {
            markARing(child, set, 0);
        }

        @SuppressWarnings("unchecked")
        private void markARing(Component child, boolean set, int which) {
            if (set) {
                if (_aring == null)
                    _aring = new Set[2];
                if (_aring[which] == null)
                    _aring[which] = new HashSet<Component>(2);
                _aring[which].add(child);
            } else if (_aring != null && _aring[which] != null && _aring[which].remove(child)
                    && _aring[which].isEmpty())
                if (_aring[which == 0 ? 1 : 0] == null) //both null
                    _aring = null;
                else
                    _aring[which] = null;
        }
    }

    public static class ForwardInfo implements java.io.Serializable {
        private final EventListener<? extends Event> listener;
        //List([target or targetPath, targetEvent, eventData])]
        private final List<TargetInfo> targets;

        private ForwardInfo(EventListener<? extends Event> listener, List<TargetInfo> targets) {
            this.listener = listener;
            this.targets = targets;
        }

        public List<TargetInfo> getTargets() {
            return targets;
        }
    }

    public static class TargetInfo implements java.io.Serializable {
        private final Object target;
        private final String event;
        private Object data;

        private TargetInfo(Object target, String event, Object data) {
            this.target = target;
            this.event = event;
            this.data = data;
        }

        public Object getTarget() {
            return target;
        }
        public String getEvent() {
            return event;
        }

        public Object getData() {
            return data;
        }
    }

    private static final Converter<EventListenerInfo, EventListener<? extends Event>> _listenerInfoConverter = new Converter<EventListenerInfo, EventListener<? extends Event>>() {
        public EventListener<? extends Event> convert(EventListenerInfo o) {
            return o.listener;
        }
    };

    private static class ExecInfo implements org.zkoss.zk.ui.sys.ExecutionInfo {
        private final Thread _thread;
        private final Event _event;
        private Method _method;
        private EventListener<? extends Event> _listener;
        private ZScript _zscript;

        private ExecInfo(Event event) {
            _thread = Thread.currentThread();
            _event = event;
        }

        public Thread getThread() {
            return _thread;
        }

        public Event getEvent() {
            return _event;
        }

        public Method getEventMethod() {
            return _method;
        }

        public EventListener<? extends Event> getEventListener() {
            return _listener;
        }

        public ZScript getEventZScript() {
            return _zscript;
        }

        public void update(Method mtd, EventListener<? extends Event> ln, ZScript zs) {
            _method = mtd;
            _listener = ln;
            _zscript = zs;
        }
    }

    // Shadow Element Implementation Start
    protected Map<Component, Integer> initIndexCacheMap(Component host) {
        Map<Component, Integer> distributedIndexInfo = getIndexCacheMap(host);
        if (distributedIndexInfo == null) {
            distributedIndexInfo = new HashMap<Component, Integer>(getChildren().size());
            ShadowElementsCtrl.setDistributedIndexInfo(host, distributedIndexInfo);
        }
        return distributedIndexInfo;
    }

    protected Map<Component, Integer> getIndexCacheMap(Component host) {
        return (Map<Component, Integer>) ShadowElementsCtrl.getDistributedIndexInfo(host);
    }

    protected void destroyIndexCacheMap(Component host) {
        ShadowElementsCtrl.setDistributedIndexInfo(host, null);
    }

    private boolean _disabledHostChanged = false;

    protected boolean isDisabledHostChanged() {
        return _disabledHostChanged;
    }
    protected void disableHostChanged() {
        _disabledHostChanged = true;
    }
    protected void enableHostChanged() {
        _disabledHostChanged = false;
    }

    private void triggerBeforeHostParentChanged(Component parent) {
        if (isDisabledHostChanged()) return;
        List<ShadowElement> shadowRoots = getShadowRoots();
        if (!shadowRoots.isEmpty()) {
            Map<Component, Integer> indexCacheMap = getIndexCacheMap(this);
            try {
                if (indexCacheMap == null) {
                    initIndexCacheMap(this);
                }
                for (ShadowElement se : new LinkedList<ShadowElement>(shadowRoots)) {
                    if (se instanceof ShadowElementCtrl) {
                        ((ShadowElementCtrl) se).beforeHostParentChanged(parent);
                    }
                }
            } finally {
                if (indexCacheMap == null) {
                    destroyIndexCacheMap(this);
                }
            }
        }
    }

    private void triggerBeforeHostChildRemoved(Component child) {
        if (isDisabledHostChanged()) return;
        List<ShadowElement> shadowRoots = getShadowRoots();
        if (!shadowRoots.isEmpty()) {
            Map<Component, Integer> indexCacheMap = getIndexCacheMap(this);
            try {
                if (indexCacheMap == null) {
                    initIndexCacheMap(this);
                }
                final int indexOf = getChildren().indexOf(child);
                for (ShadowElement se : new LinkedList<ShadowElement>(shadowRoots)) {
                    if (se instanceof ShadowElementCtrl) {
                        ((ShadowElementCtrl) se).beforeHostChildRemoved(child, indexOf);
                    }
                }
            } finally {
                if (indexCacheMap == null) {
                    destroyIndexCacheMap(this);
                }
            }
        }
    }

    private void triggerAfterHostChildRemoved(Component child) {
        List<ShadowElement> shadowRoots = getShadowRoots();
        if (!shadowRoots.isEmpty()) {
            Map<Component, Integer> indexCacheMap = getIndexCacheMap(this);
            try {
                if (indexCacheMap == null) {
                    initIndexCacheMap(this);
                }
                for (ShadowElement se : new LinkedList<ShadowElement>(shadowRoots)) {
                    if (se instanceof ShadowElementCtrl) {
                        ((ShadowElementCtrl) se).afterHostChildRemoved(child);
                    }
                }
            } finally {
                if (indexCacheMap == null) {
                    destroyIndexCacheMap(this);
                }
            }
        }
    }

    private void triggerBeforeHostChildAdded(Component child, Component insertBefore) {
        if (isDisabledHostChanged()) return;
        List<ShadowElement> shadowRoots = getShadowRoots();
        if (!shadowRoots.isEmpty()) {
            try {
                initIndexCacheMap(this);
                final int indexOfInsertBefore = insertBefore == null ? -1 : getChildren().indexOf(insertBefore);
                for (ShadowElement se : new LinkedList<ShadowElement>(shadowRoots)) {
                    if (se instanceof ShadowElementCtrl) {
                        ((ShadowElementCtrl) se).beforeHostChildAdded(child, insertBefore, indexOfInsertBefore);
                    }
                }
            } finally {
                // Always reset because the index cache will be changed after child added.
                // See: NavigationModelTest.java
                destroyIndexCacheMap(this);
            }
        }
    }

    private void triggerAfterHostChildAdded(Component child) {
        if (isDisabledHostChanged()) return;
        List<ShadowElement> shadowRoots = getShadowRoots();
        if (!shadowRoots.isEmpty()) {
            Map<Component, Integer> indexCacheMap = getIndexCacheMap(this);
            try {
                if (indexCacheMap == null) {
                    initIndexCacheMap(this);
                }
                final int indexOf = getChildren().indexOf(child);
                for (ShadowElement se : new LinkedList<ShadowElement>(shadowRoots)) {
                    if (se instanceof ShadowElementCtrl) {
                        ((ShadowElementCtrl) se).afterHostChildAdded(child, indexOf);
                    }
                }
            } finally {
                if (indexCacheMap == null) {
                    destroyIndexCacheMap(this);
                }
            }
        }
    }
    // Shadow Element Implementation End

    @SuppressWarnings("unchecked")
    public <T extends ShadowElement> List<T> getShadowRoots() {
        return _auxinf != null && _auxinf.seRoots != null ? (List<T>) _auxinf.seRoots : Collections.emptyList();
    }

    public boolean removeShadowRoot(ShadowElement shadow) {
        if (_auxinf != null && _auxinf.seRoots != null)
            if (_auxinf.seRoots.remove(shadow)) {
                shadow.detach();
                removeFromShadowIdMap(this, (Component) shadow);
                return true;
            }
        return false;
    }

    public boolean addShadowRoot(ShadowElement shadow) {
        if (shadow == null) {
            throw new IllegalArgumentException("Shadow element cannot be null!");
        }
        if (shadow.getShadowHost() == null) {
            ((ShadowElementCtrl) shadow).setShadowHost(this, null);
        } else if (shadow.getShadowHost() != this) {
            throw new IllegalAccessError("The shadow element is not belonged to this host: [" + this + "]");
        }
        AuxInfo auxinf = initAuxInfo();
        if (auxinf.seRoots == null)
            auxinf.seRoots = new LinkedList<ShadowElement>();

        //ZK-2944: this comp is shadow host, init the map
        if (_shadowIdMap == null)
            _shadowIdMap = new HashMap<String, ShadowElement>(4);

        if (!auxinf.seRoots.contains(shadow)) {
            return auxinf.seRoots.add(shadow);
        }
        return false;
    }

    public boolean addShadowRootBefore(ShadowElement shadow, ShadowElement insertBefore) {
        if (shadow == null) {
            throw new IllegalArgumentException("Shadow element cannot be null!");
        }
        if (insertBefore == null)
            return addShadowRoot(shadow);
        if (insertBefore.getShadowHost() != this)
            throw new UiException("Wrong shadow host [" + insertBefore + "]");
        AuxInfo auxinf = initAuxInfo();
        if (!auxinf.seRoots.contains(shadow)) {
            auxinf.seRoots.add(auxinf.seRoots.indexOf(insertBefore), shadow);
            return true;
        }
        return false;
    }

    //F80 - store subtree's binder annotation count
    public boolean hasBindingAnnotation() {
        return _auxinf != null && _auxinf.hasBindingAnnotInfo;
    }

    public boolean hasSubBindingAnnotation() {
        return _auxinf != null && _auxinf.subAnnotCnt > 0;
    }

    public int getSubBindingAnnotationCount() {
        return _auxinf == null ? 0 : _auxinf.subAnnotCnt;
    }

    protected void updateSubBindingAnnotationCount(int diff) {
        for (AbstractComponent node = this; node != null; node = (AbstractComponent) node.getParent())
            setSubBindingAnnotationCount(diff, node);
    }

    protected void setSubBindingAnnotationCount(int diff, AbstractComponent node) {
        AuxInfo auxInfo = node.initAuxInfo();

        // Don't update the count for itself
        if (node != this) {
            auxInfo.subAnnotCnt += diff;
        }
    }

    private void initBindingAnnotation() {
        AuxInfo auxinf = initAuxInfo();
        auxinf.hasBindingAnnotInfo = true;
        auxinf.subAnnotCnt = 1;
        auxinf.hasBindingAnnot = true;
        updateSubBindingAnnotationCount(1); // init to 1 for itself
    }

    public void enableBindingAnnotation() {
        toggleBindingAnnotationAndUpdateCount(true);
    }

    public void disableBindingAnnotation() {
        toggleBindingAnnotationAndUpdateCount(false);
    }

    private void toggleBindingAnnotationAndUpdateCount(boolean hasBindingAnnot) {
        AuxInfo auxinf = initAuxInfo();
        boolean old = auxinf.hasBindingAnnot;
        if (old != hasBindingAnnot) {
            int multiply = hasBindingAnnot ? 1 : -1;
            auxinf.hasBindingAnnot = hasBindingAnnot;
            if (auxinf.subAnnotCnt > 0) {
                updateSubBindingAnnotationCount(multiply * auxinf.subAnnotCnt);
            }
        }
    }

    private static void addToShadowIdMap(Component comp) {
        if (comp instanceof ShadowElementCtrl) {
            Component host = ((ShadowElementCtrl) comp).getShadowHostIfAny();
            if (host != null) {
                String id = comp.getId();
                AbstractComponent ac = (AbstractComponent) host;
                if (ac._shadowIdMap != null) {
                    if (ac._shadowIdMap.get(id) != null)
                        throw new InternalError("Caller shall prevent duplicated ID for shadow hosts");
                    ac._shadowIdMap.put(id, (ShadowElement) comp);
                }
            }
        }
    }

    public ShadowElement getShadowFellowIfAny(String id) {
        return _shadowIdMap == null ? null : _shadowIdMap.get(id);
    }

    /**
     * Returns if it's finished layout phase (initializing).
     * @return true if it's initialized.
     * @since 9.5.1
     */
    public boolean isInitialized() {
        return _initialized;
    }

    private static void removeFromShadowIdMap(Component comp) {
        if (comp instanceof ShadowElementCtrl) {
            Component host = ((ShadowElementCtrl) comp).getShadowHostIfAny();
            if (host != null) {
                removeFromShadowIdMap(host, comp);
            }
        }
    }

    private static void removeFromShadowIdMap(Component host, Component comp) {
        if (host != null && comp instanceof ShadowElement) {
            String id = comp.getId();
            AbstractComponent ac = (AbstractComponent) host;
            if (ac._shadowIdMap != null) {
                ac._shadowIdMap.remove(id);
            }
        }
    }
}