org.zkoss.zk.ui.AbstractComponent Maven / Gradle / Ivy
/* 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.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 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.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, Map> _clientEvents = new ConcurrentHashMap, Map>(
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 _shadowIdMap; //to speed up id only shadow selector
/** 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())
enableBindingAnnotation();
} 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())
enableBindingAnnotation();
} 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 {
public int size() {
return nChild();
}
public ListIterator 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.
*
* Default: it notifies {@link UiEngine} to update the component
* at the client (usually remove-and-add).
*
*
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;
boolean resetUuid = false;
if (_page != null) {
if (bRoot)
((AbstractPage) _page).removeRoot(this);
if (page == null && ((DesktopCtrl) _page.getDesktop()).removeComponent(this, true)
&& !(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
if (resetUuid)
_uuid = null; //reset it after everything is done since some tool might depend on it
}
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.length() > 0) {
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, false);
if (newUuid.length() == 0)
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;
}
}
_uuid = exec == null ? ANONYMOUS_ID + _anonymousId++ : 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 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(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 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(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 getWidgetOverrideNames() {
if (_auxinf != null && _auxinf.wgtovds != null)
return _auxinf.wgtovds.keySet();
return Collections.emptySet();
}
public String setWidgetAttribute(String name, String value) {
return setClientAttribute(name, value);
}
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(4);
old = _auxinf.domattrs.put(name, value);
} else
old = _auxinf != null && _auxinf.domattrs != null ? _auxinf.domattrs.remove(name) : null;
return old;
}
public String getWidgetAttribute(String name) {
return getClientAttribute(name);
}
public String getClientAttribute(String name) {
return _auxinf != null && _auxinf.domattrs != null ? _auxinf.domattrs.get(name) : null;
}
public Set getWidgetAttributeNames() {
if (_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 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 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 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 shadowRoots = ctrl.getShadowRoots();
if (!shadowRoots.isEmpty()) {
Map indexCacheMap = getIndexCacheMap();
try {
if (indexCacheMap != null) {
destroyIndexCacheMap(); // reset
}
initIndexCacheMap();
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 {
val = shadow.resolveVariable(baseChild, name, recurse);
if (val != null)
return val;
}
}
} finally {
ShadowElementsCtrl.setDistributedIndexInfo(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 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
//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)
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);
//F80 - store subtree's binder annotation count
updateSubBindingAnnotationCount(nc.initAuxInfo().subAnnotCnt);
}
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.
*
* 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.
*
*
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.
* If false, this component won't have any children, and all UUID of children
* reference back to this component.
* 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, false);
//don't recycle it (since the client might hold them)
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);
//F80 - store subtree's binder annotation count
updateSubBindingAnnotationCount(-oc.initAuxInfo().subAnnotCnt);
triggerAfterHostChildRemoved(child);
return true;
}
/** Returns whether this component can have a child.
*
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.
*
Default: instantiates and returns an instance of {@link Children}.
*/
public List getChildren() {
return (List) 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);
}
}
public void invalidatePartial() {
if (_page != null) {
getAttachedUiEngine().addInvalidate(this, "partial");
}
}
/** Causes a response to be sent to the client.
* It is the same as response(response.getOverrideKey(), response)
*
* 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.
*
*
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.
*
*
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}).
*
*
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.
*
*
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.
*
*
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.
* 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}).
*
*
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.
*
*
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.
*
*
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.
* 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.
*
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.
*
*
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.
*
*
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,
*
*
Once {@link #invalidate} is called, all invocations to {@link #smartUpdate(String, Object)}
* will then be ignored, and {@link #redraw} will be invoked later.
*
*
It can be called only in the request-processing and event-processing
* phases; excluding the redrawing phase.
*
*
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.
*
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 disable-event-thread
in zk.xml).
*
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.
*
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}.
*
If the value is a component, a special pattern will be generated
* to ensure it can be unmarshalled back correctly at the client.
*
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
* smartUpdate(attr, value, false)
.
*
*
For example, if you invoke smartUpdate("attr", "value1")
* and smartUpdate("attr", "value2")
, then only value2
* will be sent to the client.
*
However, if you invoke smartUpdate("attr", "value1", true)
* and smartUpdate("attr", "value2", true)
,
* then both value1
and value2
* will be sent to the client. In other words, wgt.setAttr("value1")
* and wgt.setAttr("value2")
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.
*
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.
*
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.
*
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.
*
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.
*
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.
*
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.
*
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
* setListener and it will be invoked as follows.
*
* wgt.setListener([evtnm, script]);
*
* 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 setOverride
as follows.
*
*
wgt.setOverride([name: script]);
*
* 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);
}
/** 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 callbacks = getCallback(name);
for (Callback callback : new ArrayList(callbacks)) {
callback.call(this);
removeCallback(name, callback);
}
}
/** Returns the widget class (a.k.a., widget type), or null if not defined.
* Default: return the widget class based on the current mold
* (by use of {@link ComponentDefinition#getWidgetClass}), or null
* if not found.
*
To override in Java, you could invoke {@link #setWidgetClass}.
* To override in ZUML, you could use the client namespace as follows.
*
<window xmlns:w="http://www.zkoss.org/2005/zk/client"
w:use="foo.MyWindow">
</window>
*
* @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 callback) {
return addCallback("redraw", callback);
}
public boolean removeRedrawCallback(Callback callback) {
return removeCallback("redraw", callback);
}
public Collection> 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>>(2);
List> list = _auxinf.callbacks.get(name);
if (list == null) {
list = new LinkedList>();
_auxinf.callbacks.put(name, list);
}
return list.add(callback);
}
public boolean removeCallback(String name, Callback callback) {
if (initAuxInfo().callbacks != null) {
List> list = _auxinf.callbacks.get(name);
if (list != null)
return list.remove(callback);
}
return false;
}
public Collection getCallback(String name) {
if (initAuxInfo().callbacks != null) {
List> 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.
* Default: It uses {@link JsContentRenderer} to render all information
* in JavaScript codes. For devices that don't support JavaScript,
* it must override this method.
*
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}).
*
*
If a derived class wants to render more properties, it can override
* {@link #renderProperties}.
*
If a derived class renders only a subset of its children
* (such as paging/cropping), it could override {@link #redrawChildren}.
*
If a deriving class wants to do something before
* {@link #renderProperties}, it has to override {@link #redraw}.
*
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> redrawCallback = getRedrawCallback();
if (!redrawCallback.isEmpty()) {
serenderer = new JSCumulativeContentRenderer();
for (Callback 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).
* Default: it invokes {@link #redraw} for all its children.
*
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.
*
*
Default: it renders {@link #getId} if it was assigned,
* and event names if listened (and listed in {@link #getClientEvents}).
*
*
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 {
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 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));
}
/** 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}).
* Default: return the collection of events
* added by {@link #getClientEvents}.
*
*
Rather than overriding this method, it is suggested
* to invoke {@link #addClientEvent} in the static
statement.
* For example,
*
public MyComponent extend HtmlBasedComponent {
* static {
* addClientEvent(MyComponent.class, "onOpen", 0);
* }
*
* @since 5.0.0
*/
public Map getClientEvents() {
for (Class cls = getClass(); cls != null; cls = cls.getSuperclass()) {
Map 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 static
clause
* when the class is loaded. For example,
* public class MyWidget extends HtmlBasedComponent {
* static {
* addClientEvent(MyWidget.class, "onFly", 0);
* }
*...
*
* For a programming language not easy to have the static
* 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.
*
*
Version History
* 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 events = _clientEvents.get(cls);
if (events == null) {
synchronized (cls) {
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 evts = _clientEvents.get(c);
if (evts != null) {
events.putAll(evts);
break;
}
}
_clientEvents.put(cls, events);
}
}
}
events.put(evtnm, new Integer(flags));
}
//Event//
@SuppressWarnings("deprecation")
public boolean addEventListener(String evtnm, EventListener extends Event> listener) {
return addEventListener(listener instanceof org.zkoss.zk.ui.event.Express ? 1000 : 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>(8);
boolean found = false;
List lis = _auxinf.listeners.get(evtnm);
final EventListenerInfo listenerInfo = new EventListenerInfo(priority, listener);
if (lis != null) {
if (Impls.duplicateListenerIgnored()) {
for (Iterator 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 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());
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 lis = _auxinf.listeners.get(evtnm);
if (lis != null) {
for (Iterator 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(4);
ForwardInfo info = _auxinf.forwards.get(orgEvent);
final List 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());
_auxinf.forwards.put(orgEvent, info);
}
tis.add(new TargetInfo(target, targetEvent, eventData));
return true;
}
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 tis = info.targets;
for (Iterator 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 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;
}
/** @deprecated As of release 6.0, replaced with {@link #getEventListeners}.
*/
public Iterator> getListenerIterator(String evtnm) {
if (_auxinf != null && _auxinf.listeners != null) {
final List lis = _auxinf.listeners.get(evtnm);
if (lis != null)
return CollectionsX.comodifiableIterator(lis, _listenerInfoConverter);
}
return CollectionsX.emptyIterator();
}
public Iterable> getEventListeners(String evtnm) {
if (_auxinf != null && _auxinf.listeners != null) {
final List lis = _auxinf.listeners.get(evtnm);
if (lis != null)
return new Iterable>() {
public Iterator> 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 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();
}
}
/** @deprecated As of release 6.0.0, replaced with
* {@link #getAnnotation(String, String)}.
*/
public Annotation getAnnotation(String annotName) {
return getAnnotation(null, annotName);
}
public Annotation getAnnotation(String propName, String annotName) {
return _auxinf != null && _auxinf.annots != null ? _auxinf.annots.getAnnotation(propName, annotName) : null;
}
public Collection getAnnotations(String propName, String annotName) {
if (_auxinf != null && _auxinf.annots != null)
return _auxinf.annots.getAnnotations(propName, annotName);
return Collections.emptyList();
}
/** @deprecated As of release 6.0.0, replaced with {@link #getAnnotations(String)}.
*/
public Collection getAnnotations() {
return getAnnotations(null);
}
public Collection getAnnotations(String propName) {
if (_auxinf != null && _auxinf.annots != null)
return _auxinf.annots.getAnnotations(propName);
return Collections.emptyList();
}
public List getAnnotatedPropertiesBy(String annotName) {
if (_auxinf != null && _auxinf.annots != null)
return _auxinf.annots.getAnnotatedPropertiesBy(annotName);
return Collections.emptyList();
}
public List 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);
}
}
}
/** @deprecated As of release 6.0.0, replaced with
* {@link #addAnnotation(String, String, Map)}
*/
public void addAnnotation(String annotName, Map annotAttrs) {
addAnnotation(null, annotName, annotAttrs);
}
public void addAnnotation(String propName, String annotName, Map 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.
* For better performance, we don't put this method to AnnotationMap.
* After all, ComponentInfo/ComponentDefinition.addAnnotation not for app
*/
private Map 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 as = new LinkedHashMap(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