zk/src/main/java/org/zkoss/zk/ui/impl/UiVisualizer.java
/* UiVisualizer.java
Purpose:
Description:
History:
Tue Jun 14 10:57:48 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.impl;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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.Map;
import java.util.Set;
import org.zkoss.zk.au.AuResponse;
import org.zkoss.zk.au.out.AuAppendChild;
import org.zkoss.zk.au.out.AuInsertAfter;
import org.zkoss.zk.au.out.AuInsertBefore;
import org.zkoss.zk.au.out.AuOuter;
import org.zkoss.zk.au.out.AuRemove;
import org.zkoss.zk.au.out.AuSetAttribute;
import org.zkoss.zk.au.out.AuUuid;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.Includer;
import org.zkoss.zk.ui.ext.Native;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.AbortingReason;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.sys.PageCtrl;
import org.zkoss.zk.ui.sys.StubComponent;
import org.zkoss.zk.ui.sys.Visualizer;
/**
* An implementation of {@link Visualizer} that works with
* {@link UiEngineImpl}.
*
* @author tomyeh
*/
/*package*/ class UiVisualizer implements Visualizer {
// private static final Logger log = LoggerFactory.getLogger(UiVisualizer.class);
/** The parent exec info. */
private final UiVisualizer _parent;
/** The associated execution. */
private final Execution _exec;
/** A set of invalidated pages. */
private Set<Page> _pgInvalid;
/** A set of removed pages. */
private Set<Page> _pgRemoved;
/** A set of invalidated components (Component). */
private final Set<Component> _invalidated = new LinkedHashSet<Component>(32);
/** A map of smart updates (Component comp, Map(String name, TimedValue(comp,name,value))). */
private final Map<Component, Map<String, TimedValue>> _smartUpdated = new HashMap<Component, Map<String, TimedValue>>(
64); //we use TimedValue for better sequence control
/** A set of new attached components. */
private final Set<Component> _attached = new LinkedHashSet<Component>(32);
/** A set of moved components (parent changed or page changed). */
private final Set<Component> _moved = new LinkedHashSet<Component>(32);
/** A set of components whose client-update is disabled. */
private Set<Component> _updDisabled;
/** A map of detached components (detached only -- not moved thereafter).
* (comp, comp's parent).
*/
private final Map<Component, Component> _detached = new LinkedHashMap<Component, Component>(32);
/** A map of UUID of detached or moved components.
* It is important since UUID might be re-used
*/
private final Map<Component, String> _uuids = new HashMap<Component, String>(32);
/** A map of components whose UUID is changed (Component, UUID). */
private Map<Component, String> _idChgd;
/** A map of responses being added(Component/Page, ResponseInfo)).
*/
private Map<Object, ResponseInfo> _responses;
/** the component that is including a new page (and then
* become the owner of the new page, if any).
*/
private Component _owner;
/** Time stamp for smart update and responses (see {@link TimedValue}). */
private int _timed;
/** if not null, it means the current executing is aborting
* and the content is reason to aborting. Its interpretation depends
* on {@link org.zkoss.zk.ui.sys.UiEngine}.
*/
private AbortingReason _aborting;
/** The counter used for smartUpdate(...append). */
private int _cntMultSU;
/** Whether the first execution is for async-update. */
private final boolean _1stau;
/** Whether it is in recovering. */
private final boolean _recovering;
/** Whether it is ending, i.e., no further update is allowed. */
private boolean _ending;
/** Whether it is disabled, i.e., ignore any updates to the client.
*/
private boolean _disabled;
/**
* Creates a root execution (without parent).
* In other words, it must be the first execution in the current request.
*
* @param asyncUpdate whether this execution is for async-update
* @param recovering whether this execution is in recovering,
* i.e., caused by {@link org.zkoss.zk.ui.sys.FailoverManager#recover}.
*/
public UiVisualizer(Execution exec, boolean asyncUpdate, boolean recovering) {
_exec = exec;
_parent = null;
_1stau = asyncUpdate;
_recovering = recovering;
}
/**
* Creates the following execution.
* The first execution must use {@link #UiVisualizer(Execution, boolean, boolean)}
*/
public UiVisualizer(UiVisualizer parent, Execution exec) {
_exec = exec;
_parent = parent;
_1stau = parent._1stau;
_recovering = false;
}
//-- Visualizer --//
public final Execution getExecution() {
return _exec;
}
public final boolean isEverAsyncUpdate() {
return _1stau;
}
public final boolean addToFirstAsyncUpdate(List<AuResponse> responses) {
if (!_1stau)
return false;
// if (log.finerable()) log.finer("Add to 1st au: "+responses);
UiVisualizer root = getRoot();
for (AuResponse response : responses)
root.addResponse(null, response);
return true;
}
private UiVisualizer getRoot() {
for (UiVisualizer uv = this;;) {
if (uv._parent == null)
return uv;
uv = uv._parent;
}
}
public boolean isRecovering() {
return _recovering;
}
public void disable() {
_disabled = true;
}
public void enable() {
_disabled = false;
}
//-- update/redraw --//
/** Returns if this component needs to be redrawn.
* <p>Note:
* <ol>
* <li>It always returns true if the current execution is not an
* asynchronous update.</li>
* <li>If its parent is invalidated, this component will be redrawn
* too, but this method returns false since {@link #addInvalidate(Component)}
* was not called against this component.</li>
* </ol>
* @since 3.0.5
*/
public boolean isInvalidated(Component comp) {
return !_exec.isAsyncUpdate(comp.getPage()) || _invalidated.contains(comp) || _attached.contains(comp)
|| _moved.contains(comp) || _detached.containsKey(comp);
//No need to check page, recovering... since it won't be
//part of _invalidated if so.
}
/** Invalidates the whole page.
*/
public void addInvalidate(Page page) {
if (_recovering || _disabled || page == null || page instanceof VolatilePage || !_exec.isAsyncUpdate(page))
return; //nothing to do
//relative fix for BUG ZK-1464
if (_ending && _pgRemoved != null && _pgRemoved.contains(page))
return;
if (_pgInvalid == null)
_pgInvalid = new LinkedHashSet<Page>(4);
_pgInvalid.add(page);
}
/** Adds an invalidated component. Once invalidated, all invocations
* to {@link #addSmartUpdate} are ignored in this execution.
*/
public void addInvalidate(Component comp) {
final Page page = comp.getPage();
if (_recovering || _disabled || page == null || page instanceof VolatilePage || !_exec.isAsyncUpdate(page)
|| isCUDisabled(comp))
return; //nothing to do
//relative fix BUG ZK-1464
if (_ending && _pgRemoved != null && _pgRemoved.contains(page))
return;
if (_ending)
throw new IllegalStateException("UI can't be modified in the rendering phase");
checkDesktop(comp);
if (_invalidated.add(comp))
_smartUpdated.remove(comp);
}
/** Ensure the use of component is correct. */
private void checkDesktop(Component comp) {
final Desktop dt = comp.getDesktop();
if (dt != null && dt != _exec.getDesktop())
throw new IllegalStateException(
"Access denied: component, " + comp + ", belongs to another desktop: " + dt);
}
/** Smart updates a component's attribute.
* Meaningful only if {@link #addInvalidate(Component)} is not called in this
* execution
* @param value the value.
* @since 5.0.2
*/
public final void addSmartUpdate(Component comp, String attr, Object value, boolean append) {
addSmartUpdate0(comp, attr, value, append, 0);
}
/**
* Adds a smart update that will be executed at the given priority.
* The higher priority, the earlier the update is executed.
* If {@link #addSmartUpdate(Component, String, Object, boolean)}
* is invoked, the priority is assumed to 0.
* @since 6.0.0
*/
public void addSmartUpdate(Component comp, String attr, Object value, int priority) {
addSmartUpdate0(comp, attr, value, false, priority);
}
private void addSmartUpdate0(Component comp, String attr, Object value, boolean append, int priority) {
if (comp == null)
throw new IllegalArgumentException();
//main fix for BUG ZK-1464
if (_ending && (comp.getPage() == null || (_pgRemoved != null && _pgRemoved.contains(comp.getPage())))) {
return;
}
final Map<String, TimedValue> respmap = getAttrRespMap(comp);
if (respmap != null)
respmap.put(append ? attr + ":" + _cntMultSU++ : attr,
new TimedValue(_timed++, comp, attr, value, priority));
}
/**
* Clears all existing smart updates that belong to the given component.
* @since 10.0.0
*/
public void clearSmartUpdate(Component comp) {
if (comp == null)
throw new IllegalArgumentException();
_smartUpdated.remove(comp);
}
/** Sets whether to disable the update of the client widget.
* By default, if a component is attached to a page, modifications that
* change the visual representation will be sent to the client to
* ensure the consistency.
*
* @return whether it has been disabled before this invocation, i.e.,
* the previous disable status
* @since 3.6.2
*/
public boolean disableClientUpdate(Component comp, boolean disable) {
if (disable) {
if (_updDisabled == null)
_updDisabled = new HashSet<Component>(4);
return !_updDisabled.add(comp);
}
final boolean ret = _updDisabled != null && _updDisabled.remove(comp);
if (ret && _updDisabled.isEmpty())
_updDisabled = null;
return ret;
}
private final boolean isCUDisabled(Component comp) {
if (_updDisabled != null) {
//no need to check comp.getPage() since it was checked before calling
for (; comp != null; comp = comp.getParent())
if (_updDisabled.contains(comp))
return true;
}
return false;
}
/** Returns the response map for the specified attribute, or null if
* nothing to do.
*/
private Map<String, TimedValue> getAttrRespMap(Component comp) {
final Page page = comp.getPage();
if (_recovering || _disabled || page == null || page instanceof VolatilePage || !_exec.isAsyncUpdate(page)
|| _invalidated.contains(comp) || isCUDisabled(comp))
return null; //nothing to do
if (_ending)
throw new IllegalStateException("UI can't be modified in the rendering phase");
checkDesktop(comp);
Map<String, TimedValue> respmap = _smartUpdated.get(comp);
if (respmap == null)
_smartUpdated.put(comp, respmap = new HashMap<String, TimedValue>());
return respmap;
}
/** Called to update (redraw) a component, when a component is moved.
* If a component's page or parent is changed, this method need to be
* called only once for the top one.
*
* @param oldparent the parent before moved
* @param oldpg the page before moved
* @param newpg the page after moved
*/
public void addMoved(Component comp, Component oldparent, Page oldpg, Page newpg) {
if (_recovering || _disabled || (newpg == null && oldpg == null)
|| (newpg == null && (oldpg instanceof VolatilePage || !_exec.isAsyncUpdate(oldpg))) //detach from loading pg
|| (oldpg == null && (newpg instanceof VolatilePage || !_exec.isAsyncUpdate(newpg))) //attach to loading pg
|| isCUDisabled(comp) || (oldparent != null && isCUDisabled(oldparent)))
return; //to avoid redundant AuRemove
if (_ending)
throw new IllegalStateException("UI can't be modified in the rendering phase");
snapshotUuid(comp);
if (oldpg == null && !_moved.contains(comp) && !_detached.containsKey(comp)) { //new attached
_attached.add(comp);
//note: we cannot examine _exec.isAsyncUpdate here because
//comp.getPage might be ready when this method is called
} else if (newpg == null && !_moved.contains(comp)) {
if (!_attached.remove(comp))
_detached.put(comp, oldparent); //new detached
//ignore if attach and then detach
} else {
_moved.add(comp);
_attached.remove(comp);
_detached.remove(comp);
}
}
/** Called before changing the component's UUID.
* @since 5.0.3
*/
public void addUuidChanged(Component comp) {
//Algorithm of handling UUID change:
//1. If it belongs a new page, nothing to do (since there is no client widget)
//2. If not, it generates AuUuid of all modified UUID before generating
//any other responses such that client's UUID will be corrected first
if (_exec.isAsyncUpdate(comp.getPage()) //only if not belong to a new page
&& (_idChgd == null || !_idChgd.containsKey(comp)) && !isCUDisabled(comp)) {
if (_idChgd == null)
_idChgd = new LinkedHashMap<Component, String>(23);
_idChgd.put(comp, comp.getUuid());
}
}
/** Adds a response directly by using {@link AuResponse#getOverrideKey}
* as the override key.
* In other words, it is the same as <code>addResponse(resposne.getOverrideKey(), response)</code>
* <p>If the response is component-dependent, {@link AuResponse#getDepends}
* must return a component. And, if the component is removed, the response
* is removed, too.
* @since 5.0.2
*/
public void addResponse(AuResponse response) {
addResponse(response.getOverrideKey(), response);
}
/** Adds a response directly (which will be returned when
* {@link #getResponses} is called).
*
* <p>If the response is component-dependent, {@link AuResponse#getDepends}
* must return a component. And, if the component is removed, the response
* is removed, too.
*
* @param key could be anything. If null, the response is appended.
* If not null, 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.
* @see #addResponse(AuResponse)
*/
public void addResponse(String key, AuResponse response) {
addResponse(key, response, 0);
}
/** Adds a response directly with the given priority.
* The higher priority, the earlier the update is executed.
* The priority of {@link #addResponse(String, AuResponse)}
* and {@link #addResponse(AuResponse)} is assumed to be 0.
* @since 6.0.1
*/
public void addResponse(String key, AuResponse response, int priority) {
if (response == null)
throw new IllegalArgumentException();
//relative fix BUG ZK-1464
if (_ending) {
Object dps = response.getDepends();
if (dps == null && _owner == null) // Bug ZK-1708: if the response is generated inside Includer, should not return
return;
if (dps instanceof Page && _pgRemoved != null && _pgRemoved.contains((Page) dps)) {
return;
}
if (dps instanceof Component) {
Component p = (Component) dps;
if (p.getPage() == null || (_pgRemoved != null && _pgRemoved.contains(p.getPage()))) {
return;
}
}
}
final Object depends = response.getDepends(); //Page or Component
if (depends instanceof Component && isCUDisabled((Component) depends))
return; //nothing to do
if (_responses == null)
_responses = new HashMap<Object, ResponseInfo>();
ResponseInfo ri = _responses.get(depends);
if (ri == null)
_responses.put(depends, ri = new ResponseInfo());
final TimedValue tval = new TimedValue(_timed++, response, priority);
if (key != null) {
ri.values.put(key, tval); //overwrite
} else {
ri.keyless.add(tval); //don't overwrite
}
}
/** Process {@link Cropper} by removing cropped invalidates and so on.
*/
private Map<Component, Set<? extends Component>> doCrop() {
final Map<Component, Set<? extends Component>> croppingInfos = new HashMap<Component, Set<? extends Component>>();
crop(_attached, croppingInfos, false);
crop(_smartUpdated.keySet(), croppingInfos, false);
if (_responses != null)
crop(_responses.keySet(), croppingInfos, true);
crop(_invalidated, croppingInfos, false);
return croppingInfos;
}
/** Crop attached and moved.
*/
private void crop(Set coll, Map<Component, Set<? extends Component>> croppingInfos, boolean bResponse) {
for (Iterator it = coll.iterator(); it.hasNext();) {
final Object o = it.next();
if (!(o instanceof Component))
continue;
final Component comp = (Component) o;
final Page page = comp.getPage();
if (page == null || !_exec.isAsyncUpdate(page)) {
if (!bResponse)
it.remove(); //just in case
continue;
}
for (Component p, c = comp; (p = c.getParent()) != null; c = p) {
final Set<? extends Component> avail = getAvailableAtClient(p, croppingInfos);
if (avail != null) {
if (!avail.contains(c)) {
it.remove();
break;
}
p = ((Cropper) ((ComponentCtrl) p).getExtraCtrl()).getCropOwner();
if (p == null)
break;
}
}
}
}
/** Returns the available children, or null if no cropping.
*/
private static Set<? extends Component> getAvailableAtClient(Component comp,
Map<Component, Set<? extends Component>> croppingInfos) {
final Object xc = ((ComponentCtrl) comp).getExtraCtrl();
if (xc instanceof Cropper) {
//we don't need to check isCropper first since its component's job
//to ensure the consistency
Set<? extends Component> crop = croppingInfos.get(comp);
if (crop == EMPTY_CROP)
return null;
if (crop != null)
return crop;
crop = ((Cropper) xc).getAvailableAtClient();
croppingInfos.put(comp, crop != null ? crop : EMPTY_CROP);
return crop;
}
return null;
}
private static final Set<Component> EMPTY_CROP = new HashSet<Component>(1);
/** Prepares {@link #_pgRemoved} to contain set of pages that will
* be removed.
*/
private void checkPageRemoved(Set<Component> removed) {
//1. scan once
final Desktop desktop = _exec.getDesktop();
Set<Page> pages = null;
for (Page page : desktop.getPages()) {
final Component owner = ((PageCtrl) page).getOwner();
if (owner != null) { //included
final Page ownerPage = owner.getPage();
if (ownerPage == null //detached
|| (_pgInvalid != null && _pgInvalid.contains(ownerPage))
|| isAncestor(_invalidated, owner, true) || isAncestor(_attached, owner, true)
|| isAncestor(removed, owner, true)) {
addPageRemoved(page);
} else {
if (pages == null)
pages = new LinkedHashSet<Page>();
pages.add(page);
}
}
}
if (_pgRemoved == null || pages == null)
return;
//done if no page is removed or no more included page
//2. if a page is ever removed, it might cause chain effect
//so we have to loop until nothing changed
boolean pgRemovedFound;
do {
pgRemovedFound = false;
for (Iterator<Page> it = pages.iterator(); it.hasNext();) {
final Page page = it.next();
final Component owner = ((PageCtrl) page).getOwner();
if (_pgRemoved.contains(owner.getPage())) {
it.remove();
addPageRemoved(page);
pgRemovedFound = true;
}
}
} while (pgRemovedFound); //loop due to chain effect
}
private void addPageRemoved(Page page) {
if (page == null || page instanceof VolatilePage)
return;
if (_pgRemoved == null)
_pgRemoved = new LinkedHashSet<Page>();
_pgRemoved.add(page);
if (_pgInvalid != null)
_pgInvalid.remove(page);
// if (log.isDebugEnabled()) log.debug("Page removed: "+page);
}
/** Clears components if it belongs to invalidated or removed page. */
private void clearInInvalidPage(Collection<Component> coll) {
for (Iterator<Component> it = coll.iterator(); it.hasNext();) {
final Component comp = it.next();
final Page page = comp.getPage();
if (page != null && ((_pgRemoved != null && _pgRemoved.contains(page))
|| (_pgInvalid != null && _pgInvalid.contains(page))))
it.remove();
}
}
/** Returns whether any component in coll is an ancestor of comp.
* @param includingEquals whether to return true if a equals B
*/
private boolean isAncestor(Collection<Component> coll, Component comp, boolean includingEquals) {
for (Iterator<Component> it = coll.iterator(); it.hasNext();) {
final Component c = it.next();
if ((includingEquals || c != comp) && Components.isAncestor(c, comp))
return true;
}
return false;
}
/** Returns a list of {@link AuResponse} according to what components
* are invalidated and attached.
* @param renderedComps used to return the components that are rendered.
* It is ignored if null. If not null, it must be mutable and
* this method will add the topmost rendered components to this collection.
* @since 6.0.0
*/
public List<AuResponse> getResponses(Collection<Component> renderedComps) throws IOException {
_ending = true; //no more modifying UI (invalidate/addSmartUpdate...)
/* if (log.finerable())
log.finer("ei: "+this+"\nInvalidated: "+_invalidated+"\nSmart Upd: "+_smartUpdated
+"\nAttached: "+_attached+"\nMoved:"+_moved+"\nResponses:"+_responses
+"\npgInvalid: "+_pgInvalid +"\nUuidChanged: "+_idChgd);
*/
final List<AuResponse> responses = new LinkedList<AuResponse>();
//0. Correct the UUID at the client first
if (_idChgd != null) {
for (Map.Entry<Component, String> me : _idChgd.entrySet()) {
final Component comp = me.getKey();
if (!_attached.contains(comp))
responses.add(new AuUuid(comp, me.getValue()));
}
_idChgd = null; //just in case
}
//1. process dead components, cropping and the removed page
final Map<Component, Set<? extends Component>> croppingInfos;
{
//1a. handle _detached to remove unnecessary detach
doDetached();
//after call, _detached is merged to _moved
//1b. handle _moved
//The reason to remove first: some insertion might fail if the old
//components are not removed yet
//Also, we have to remove both parent and child because, at
//the client, they might not be parent-child relationship
Set<Component> removed = doMoved(responses);
//after called, _moved is cleared (add to _attached if necessary)
//And, AuRemove is generated (we have to generate AuRemove first,
//since UUID might be reused)
//1c. remove redundant
removeRedundant();
//1d. process Cropper
croppingInfos = doCrop();
//1d. prepare removed pages and optimize for invalidate or removed pages
checkPageRemoved(removed); //maintain _pgRemoved for pages being removed
}
//2. Process removed and invalid pages
//2a. clean up _invalidated and others belonging to invalid pages
if (_pgInvalid != null && _pgInvalid.isEmpty())
_pgInvalid = null;
if (_pgRemoved != null && _pgRemoved.isEmpty())
_pgRemoved = null;
if (_pgInvalid != null || _pgRemoved != null) {
clearInInvalidPage(_invalidated);
clearInInvalidPage(_attached);
clearInInvalidPage(_smartUpdated.keySet());
}
//2b. remove pages. Note: we don't need to generate rm, because they
//are included pages.
if (_pgRemoved != null) {
final DesktopCtrl dtctl = (DesktopCtrl) _exec.getDesktop();
for (Page page : _pgRemoved)
dtctl.removePage(page);
}
//3. generate response for invalidated pages
if (_pgInvalid != null) {
for (Page page : _pgInvalid) {
if (renderedComps != null)
renderedComps.addAll(page.getRoots());
responses.add(new AuOuter(page, redraw(page)));
}
}
/* if (log.finerable())
log.finer("After removing redundant: invalidated: "+_invalidated
+"\nAttached: "+_attached+"\nSmartUpd:"+_smartUpdated);
*/
//4. process special interfaces
//5. generate replace for invalidated
for (Component comp : _invalidated) {
if (renderedComps != null)
renderedComps.add(comp);
responses.add(new AuOuter(comp, redraw(comp)));
}
//6. add attached components (including setParent)
//Due to cyclic references, we have to process all siblings
//at the same time
final List<Set<Component>> desktops = new LinkedList<Set<Component>>();
final Component[] attached = _attached.toArray(new Component[_attached.size()]);
for (int j = 0; j < attached.length; ++j) {
final Component comp = attached[j];
//Note: attached comp might change from another page to
//the one being created. In this case, no need to add
if (comp != null) {
final Page page = comp.getPage();
if (page != null && _exec.isAsyncUpdate(page)) {
final Component parent = comp.getParent();
final Set<Component> newsibs = new LinkedHashSet<Component>(32);
newsibs.add(comp);
desktops.add(newsibs);
for (int k = j + 1; k < attached.length; ++k) {
final Component ck = attached[k];
if (ck != null && ck.getParent() == parent) {
newsibs.add(ck);
attached[k] = null;
}
}
}
}
}
for (Set<Component> newsibs : desktops) {
if (renderedComps != null)
renderedComps.addAll(newsibs);
addResponsesForCreatedPerSiblings(responses, newsibs, croppingInfos);
}
//7. Adds smart updates and response at once based on their time stamp
final List<TimedValue> tvals = new LinkedList<TimedValue>();
for (Map<String, TimedValue> attrs : _smartUpdated.values()) {
tvals.addAll(attrs.values());
}
if (_responses != null) {
for (Map.Entry<Object, ResponseInfo> me : _responses.entrySet()) {
final Object depends = me.getKey();
if (depends instanceof Component) {
final Component cd = (Component) depends;
if (cd.getPage() == null || isCUDisabled(cd))
continue;
}
ResponseInfo ri = me.getValue();
tvals.addAll(ri.keyless);
tvals.addAll(ri.values.values());
}
}
if (!tvals.isEmpty()) {
final TimedValue[] tvs = tvals.toArray(new TimedValue[tvals.size()]);
Arrays.sort(tvs);
for (int j = 0; j < tvs.length; ++j)
responses.add(tvs[j].getResponse());
}
//any aborting reason
//Note: we don't give up other responses (Bug 1647085)
if (_aborting != null) {
final AuResponse abtresp = _aborting.getResponse();
if (abtresp != null)
responses.add(abtresp); //add to the end
}
//free memory
_invalidated.clear();
_smartUpdated.clear();
_attached.clear();
_uuids.clear();
_pgInvalid = _pgRemoved = null;
_responses = null;
// if (log.isDebugEnabled()) log.debug("Return responses: "+responses);
// System.out.println("Return responses: "+responses);
return responses;
}
/** Process detached components.
* After called, _detached is merged backed to _moved if it is required
*/
private void doDetached() {
l_out: for (Map.Entry<Component, Component> me : _detached.entrySet()) {
Component p = me.getValue();
for (; p != null; p = p.getParent())
if (_moved.contains(p) || _detached.containsKey(p) || _invalidated.contains(p) || _attached.contains(p))
continue l_out; //don't merge (ignore it)
_moved.add(me.getKey()); //merge
}
_detached.clear(); //no longer required
}
/** process moved components.
*
* <p>After called, _moved becomes empty.
* If they are removed, corresponding AuRemove are generated.
* If not, they are added to _attached.
*
* @return the dead components (i.e., not belong to any page)
*/
private Set<Component> doMoved(List<AuResponse> responses) {
//Remove components that have to removed from the client
final Set<Component> removed = new LinkedHashSet<Component>();
for (Component comp : _moved) {
final Page page = comp.getPage();
if (page == null) {
removed.add(comp);
if (_responses != null)
_responses.remove(comp);
_invalidated.remove(comp);
_smartUpdated.remove(comp);
responses.add(new AuRemove(uuid(comp)));
//Use the original UUID is important since it might be reused
} else { //page != null
if (_exec.isAsyncUpdate(page))
responses.add(new AuRemove(uuid(comp)));
//Use the original UUID is important since it might be reused
_attached.add(comp);
//copy to _attached since we handle them later in the same way
}
}
_moved.clear(); //no longer required
return removed;
}
/** Stores the original UUID of the specified component.
*/
private void snapshotUuid(Component comp) {
if (!_uuids.containsKey(comp))
_uuids.put(comp, comp.getUuid());
}
/** Returns the original UUID of the specified component.
*/
private String uuid(Component comp) {
final String uuid = _uuids.get(comp);
return uuid != null ? uuid : comp.getUuid();
}
/** Adds responses for a set of siblings which is new attached (or
* parent is changed).
*/
private static void addResponsesForCreatedPerSiblings(List<AuResponse> responses, Set<Component> newsibs,
Map<Component, Set<? extends Component>> croppingInfos) throws IOException {
final Component parent;
final Page page;
{
final Component comp = newsibs.iterator().next();
parent = comp.getParent();
page = comp.getPage();
}
Collection<? extends Component> sibs = parent != null ? getAvailableAtClient(parent, croppingInfos) : null;
// if (log.finerable()) log.finer("All sibs: "+sibs+" newsibs: "+newsibs);
/* Algorithm: 5.0.7
1. Groups newsibs
2. For each group, see if it is better to use AuAppendChild/AuInsertBefore/AuInsertAfter
(Note: newsibs might not be ordered correctly, so we have to go through nextGroupedSiblings)
*/
for (List<Component> group; (group = nextGroupedSiblings(newsibs)) != null;) {
final Collection<String> contents = redrawComponents(group);
final Component last = group.get(group.size() - 1);
Component nxt, prv;
if ((nxt = last.getNextSibling()) == null || (sibs != null && !sibs.contains(nxt))) { //nextsib not available at client
if (parent != null //since page might not available, we try AuInsertAfter first if parent is null
&& !(parent instanceof Native) && !(parent instanceof StubComponent)) { //parent valid
responses.add(new AuAppendChild(parent, contents));
} else {
final Component first = group.get(0);
if ((prv = first.getPreviousSibling()) != null && (sibs == null || sibs.contains(prv)) //prv is available
&& !(prv instanceof Native) && !(prv instanceof StubComponent)) { //prv valid
responses.add(new AuInsertAfter(prv, contents));
} else {
if (parent != null)
throw new UiException("Adding child to native or stubs not allowed: " + parent);
responses.add(new AuAppendChild(page, contents));
}
}
} else if (nxt instanceof Native || nxt instanceof StubComponent) { //native
final Component first = group.get(0);
if ((prv = first.getPreviousSibling()) == null || (sibs != null && !sibs.contains(prv))) //prv is not available
throw new UiException("Inserting a component before a native one not allowed: " + nxt);
//prv is available, so use AuInsertAfter prv instead
responses.add(new AuInsertAfter(prv, contents));
} else {
//use AuInsertBefore nxt
responses.add(new AuInsertBefore(nxt, contents));
}
}
}
private static List<Component> nextGroupedSiblings(Set<Component> newsibs) {
if (newsibs.isEmpty())
return null;
final List<Component> group = new LinkedList<Component>();
final Component first;
{
final Iterator<Component> it = newsibs.iterator();
first = it.next();
it.remove();
}
group.add(first);
for (Component c = first; (c = c.getNextSibling()) != null && newsibs.remove(c);) //next is also new
group.add(c);
for (Component c = first; (c = c.getPreviousSibling()) != null && newsibs.remove(c);) //prev is also new
group.add(0, c);
return group;
}
/** Removes redundant components in _invalidated, _smartUpdated and _attached.
*/
private void removeRedundant() {
int initsz = (_invalidated.size() + _attached.size()) / 2 + 30;
final Set<Component> ins = new HashSet<Component>(initsz), //one of ancestor in _invalidated or _attached
outs = new HashSet<Component>(initsz); //none of ancestor in _invalidated nor _attached
final List<Component> ancs = new ArrayList<Component>(50);
//process _invalidated
for (Iterator<Component> it = _invalidated.iterator(); it.hasNext();) {
Component p = it.next();
if (_attached.contains(p)) { //attached has higher priority
it.remove();
continue;
}
boolean removed = false;
while ((p = p.getParent()) != null) { //don't check p in _invalidated
if (outs.contains(p)) //checked
break;
if (ins.contains(p) || _invalidated.contains(p) || _attached.contains(p)) {
it.remove();
removed = true;
break;
}
ancs.add(p);
}
if (removed)
ins.addAll(ancs);
else
outs.addAll(ancs);
ancs.clear();
}
//process _attached
for (Iterator<Component> it = _attached.iterator(); it.hasNext();) {
Component p = it.next();
boolean removed = false;
while ((p = p.getParent()) != null) { //don't check p in _attached
if (outs.contains(p)) //checked
break;
if (ins.contains(p) || _invalidated.contains(p) || _attached.contains(p)) {
it.remove();
removed = true;
break;
}
ancs.add(p);
}
if (removed)
ins.addAll(ancs);
else
outs.addAll(ancs);
ancs.clear();
}
//process _smartUpdated
for (Iterator<Component> it = _smartUpdated.keySet().iterator(); it.hasNext();) {
Component p = it.next();
boolean removed = false, first = true;
for (; p != null; p = p.getParent()) { //check p in _smartUpdated
if (outs.contains(p)) //checked
break;
if (ins.contains(p) || _invalidated.contains(p) || _attached.contains(p)) {
it.remove();
removed = true;
break;
}
if (first)
first = false; //No need to add 1st p
else
ancs.add(p);
}
if (removed)
ins.addAll(ancs);
else
outs.addAll(ancs);
ancs.clear();
}
}
/** Redraw the specified component into a string.
*/
private static String redraw(Component comp) throws IOException {
final StringWriter out = new StringWriter(1024 * 8);
((ComponentCtrl) comp).redraw(out);
return out.toString();
}
/** Redraws the whole page. */
private static String redraw(Page page) throws IOException {
final StringWriter out = new StringWriter(1024 * 8);
((PageCtrl) page).redraw(out);
return out.toString();
}
private static List<String> redrawComponents(Collection<Component> comps) throws IOException {
final List<String> list = new LinkedList<String>();
for (Component comp : comps)
list.add(redraw(comp));
return list;
}
/** Called before a component redraws itself if the component might
* include another page.
* <p>Since 5.0.6, the owner must implement {@link Includer}.
* @return the previous owner
* @since 5.0.0
*/
public Component setOwner(Component comp) {
Component old = _owner;
if (comp != null && !(comp instanceof Includer))
throw new IllegalArgumentException(comp.getClass() + " must implement " + Includer.class.getName());
_owner = comp;
return old;
}
/** Returns the owner component for this execution, or null if
* this execution is not owned by any component.
* The owner is the top of the stack pushed by {@link #setOwner}.
* <p>Note: the owner, if not null, must implement {@link Includer}.
*/
public Component getOwner() {
return _owner;
}
/** Used to hold smart update and response with a time stamp.
*/
private static class TimedValue implements Comparable {
private final int _priority;
private final int _timed;
private final AuResponse _response;
private TimedValue(int timed, AuResponse response, int priority) {
_timed = timed;
_response = response;
_priority = priority;
}
private TimedValue(int timed, Component comp, String name, Object value, int priority) {
_timed = timed;
_response = new AuSetAttribute(comp, name, value);
_priority = priority;
}
public String toString() {
return '(' + _timed + ":" + _response + ')';
}
public int compareTo(Object o) {
final TimedValue tv = (TimedValue) o;
return _priority == tv._priority ? _timed > tv._timed ? 1 : _timed == tv._timed ? 0 : -1
: _priority > tv._priority ? -1 : 1; //higher priority, earlier (smaller)
}
/** Returns the response representing this object. */
private AuResponse getResponse() {
return _response;
}
}
/** Sets the reason to abort the current execution.
* if not null, it means the current execution is aborting
* and the specified argument is the reason to aborting.
* Its interpretation depends on {@link org.zkoss.zk.ui.sys.UiEngine}.
*
* <p>Note: if setAbortingReason is ever set with non-null, you
* CANNOT set it back to null.
*
* <p>The aborting flag means no more processing, i.e., dropping pending
* requests, events, and rendering.
*
* <p>After call this method, you shall not keep processing the page
* because the rendering is dropped and the client is out-of-sync
* with the server.
*
* <p>This method doesn't really abort pending events and requests.
* It just set a flag, {@link #getAbortingReason}, and it is
* {@link org.zkoss.zk.ui.sys.UiEngine}'s job to detect this flag
* and handling it properly.
*/
public void setAbortingReason(AbortingReason reason) {
if (_aborting != null && reason == null)
throw new IllegalStateException("Aborting reason is set and you cannot clear it");
//Reason: some event or request might be skipped
//so clearing it might cause unexpected results
_aborting = reason;
}
/** Returns the reason to aborting, or null if no aborting at all.
*
* @see #setAbortingReason
*/
public AbortingReason getAbortingReason() {
return _aborting;
}
/** Returns whether it is aborting.
*
* <p>The execution is aborting if {@link #getAbortingReason} returns
* not null and the returned reason's {@link AbortingReason#isAborting}
* is true.
*
* <p>Note: {@link Execution#isVoided} means the execution is voided
* and no output shall be generated. The request is taken charged
* by other servlet.
* On the other hand, {@link #isAborting} means the execution
* is aborting and the output shall still be generated (and sent).
* The request is still taken charged by this execution.
*/
public boolean isAborting() {
return _aborting != null && _aborting.isAborting();
}
private static class ResponseInfo {
/** A list of keyless responses. */
private final List<TimedValue> keyless = new LinkedList<TimedValue>();
/** A map of key and responses. */
private final Map<String, TimedValue> values = new HashMap<String, TimedValue>(4);
}
}