/**
*
*/
package org.zkoss.zk.ui.select.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.zkoss.lang.Strings;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.HtmlShadowElement;
import org.zkoss.zk.ui.IdSpace;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.ShadowElement;
import org.zkoss.zk.ui.select.impl.Selector.Combinator;
import org.zkoss.zk.ui.sys.ComponentCtrl;
/**
* An implementation of Iterator<Component> that realizes the selector matching
* algorithm. The iteration is lazily evaluated. i.e. The iterator will not
* perform extra computation until .next() is called.
* @since 6.0.0
* @author simonpai
*/
public class ComponentIterator implements Iterator<Component> {
private final Page _page;
private final Component _root;
private final List<Selector> _selectorList;
private final int _posOffset;
private final boolean _allIds;
private final boolean _lookingForShadow;
private final Map<String, PseudoClassDef> _localDefs = new HashMap<String, PseudoClassDef>();
private Component _offsetRoot;
private ComponentMatchCtx _currCtx;
/**
* Create an iterator which selects from all the components in the page.
* @param page the reference page for selector
* @param selector the selector string
*/
public ComponentIterator(Page page, String selector) {
this(page, null, selector);
}
/**
* Create an iterator which selects from all the descendants of a given
* component, including itself.
* @param root the reference component for selector
* @param selector the selector string
*/
public ComponentIterator(Component root, String selector) {
this(root.getPage(), root, selector);
}
private ComponentIterator(Page page, Component root, String selector) {
if (page == null && root == null)
throw new IllegalArgumentException("Page or root component cannot be null.");
if (Strings.isEmpty(selector))
throw new IllegalArgumentException("Selector string cannot be empty.");
_selectorList = new Parser()
.parse(selector.replaceAll("^::shadow", "*::shadow").replaceAll("::shadow", " > ::shadow"));
if (_selectorList.isEmpty())
throw new IllegalStateException("Empty selector");
_posOffset = getCommonSeqLength(_selectorList);
_allIds = isAllIds(_selectorList, _posOffset);
_lookingForShadow = lookingForShadow(_selectorList);
_root = root;
_page = page;
}
private static int getCommonSeqLength(List<Selector> list) {
List<String> strs = null;
int max = 0;
for (Selector selector : list) {
if (strs == null) {
strs = new ArrayList<String>();
for (SimpleSelectorSequence seq : selector)
if (!Strings.isEmpty(seq.getId())) {
strs.add(seq.toString());
strs.add(seq.getCombinator().toString());
} else
break;
max = strs.size();
} else {
int i = 0;
for (SimpleSelectorSequence seq : selector)
if (i >= max || Strings.isEmpty(seq.getId()) || !strs.get(i++).equals(seq.toString())
|| !strs.get(i++).equals(seq.getCombinator().toString()))
break;
if (i-- < max)
max = i;
}
}
return (max + 1) / 2;
}
private static boolean isAllIds(List<Selector> list, int offset) {
for (Selector s : list)
if (s.size() > offset)
return false;
return true;
}
private static boolean lookingForShadow(List<Selector> list) {
for (Selector s : list) {
for (SimpleSelectorSequence seq : s) {
if (!seq.getPseudoElements().isEmpty()) {
return true;
}
}
}
return false;
}
// custom pseudo class definition //
/**
* Add or set pseudo class definition.
* @param name the pseudo class name
* @param def the pseudo class definition
*/
public void setPseudoClassDef(String name, PseudoClassDef def) {
_localDefs.put(name, def);
}
/**
* Remove a pseudo class definition.
* @param name the pseudo class name
* @return the original definition
*/
public PseudoClassDef removePseudoClassDef(String name) {
return _localDefs.remove(name);
}
/**
* Clear all custom pseudo class definitions.
*/
public void clearPseudoClassDefs() {
_localDefs.clear();
}
// iterator //
private boolean _ready = false;
private Component _next;
private int _index = -1;
/**
* Return true if it has next component.
*/
public boolean hasNext() {
loadNext();
return _next != null;
}
/**
* Return the next matched component. A NoSuchElementException will be
* throw if next component is not available.
*/
public Component next() {
if (!hasNext())
throw new NoSuchElementException();
_ready = false;
return _next;
}
/**
* Throws UnsupportedOperationException.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Return the next matched component, but the iteration is not proceeded.
*/
public Component peek() {
if (!hasNext())
throw new NoSuchElementException();
return _next;
}
/**
* Return the index of the next component.
*/
public int nextIndex() {
return _ready ? _index : _index + 1;
}
// helper //
private void loadNext() {
if (_ready)
return;
_next = seekNext();
_ready = true;
}
private Component seekNext() {
_currCtx = _index < 0 ? buildRootCtx() : buildNextCtx();
while (_currCtx != null && !_currCtx.isMatched())
_currCtx = buildNextCtx();
if (_currCtx != null) {
_index++;
return _currCtx.getComponent();
}
return null;
}
private ComponentMatchCtx buildRootCtx() {
Component rt = _root == null ? _page.getFirstRoot() : _root;
if (_posOffset > 0) {
Selector selector = _selectorList.get(0);
for (int i = 0; i < _posOffset; i++) {
SimpleSelectorSequence seq = selector.get(i);
Component rt2 = null;
// ZK-2944 cannot process shadow roots here, skip them
if (!seq.getPseudoElements().isEmpty()) { //::shadow
if (!((ComponentCtrl) rt).getShadowRoots().isEmpty() && seq.getId() != null) { //rt is shadow host and host id is given
rt2 = (Component) ((ComponentCtrl) rt).getShadowFellowIfAny(seq.getId());
} else {
continue;
}
} else {
rt2 = rt.getFellowIfAny(seq.getId());
}
if (rt2 == null)
return null;
// match local properties
if (!ComponentLocalProperties.matchType(rt2, seq.getType())
|| !ComponentLocalProperties.matchClasses(rt2, seq.getClasses())
|| !ComponentLocalProperties.matchAttributes(rt2, seq.getAttributes())
|| !ComponentLocalProperties.matchPseudoClasses(rt2, seq.getPseudoClasses(), _localDefs))
return null;
// check combinator for second and later jumps
if (i > 0) {
switch (selector.getCombinator(i - 1)) {
case DESCENDANT:
if (!isDescendant(rt2, rt))
return null;
break;
case CHILD:
if (rt2 instanceof ShadowElement) {
if (((ShadowElement) rt2).getShadowHost() != rt)
return null;
} else if (rt2.getParent() != rt) {
return null;
}
break;
case GENERAL_SIBLING:
if (!isGeneralSibling(rt2, rt))
return null;
break;
case ADJACENT_SIBLING:
if (rt2.getPreviousSibling() != rt)
return null;
break;
}
}
rt = rt2;
}
_offsetRoot = rt.getParent();
}
ComponentMatchCtx ctx = new ComponentMatchCtx(rt, _selectorList);
if (_posOffset > 0)
for (Selector selector : _selectorList)
ctx.setQualified(selector.getSelectorIndex(), _posOffset - 1);
else
matchLevel0(ctx);
//System.out.println(ctx); // TODO: debugger
return ctx;
}
private ComponentMatchCtx buildNextCtx() {
if (_allIds)
return null;
// TODO: how to skip tree branches
// traverse shadow element tree only when selecting shadow elements
if (_lookingForShadow && _currCtx.isShadowHost()) {
return buildFirstShadowChildCtx(_currCtx);
}
// traverse non shadow component tree
if (_currCtx.getComponent().getFirstChild() != null) {
return buildFirstChildCtx(_currCtx);
}
while (_currCtx.getComponent().getNextSibling() == null) {
if (_lookingForShadow) {
ShadowElement se = getNextShadowRootSibling();
if (se != null) {
return buildNextShadowSiblingCtx(_currCtx, se);
}
}
_currCtx = _currCtx.getParent();
if (_currCtx == null || _currCtx.getComponent() == (_posOffset > 0 ? _offsetRoot : _root))
return null; // reached root
}
return buildNextSiblingCtx(_currCtx);
}
// shadow root have no parent, only host. Retrieve sibling from host's seRoots, if there is any.
private ShadowElement getNextShadowRootSibling() {
Component comp = _currCtx.getComponent();
if (comp instanceof ShadowElement) {
Component host = ((ShadowElement) comp).getShadowHost();
if (host instanceof ComponentCtrl) {
List<ShadowElement> seRoots = ((ComponentCtrl) host).getShadowRoots();
if (seRoots != null && seRoots.size() > 1) { //if equal to 1, then it is the current comp itelf
int index = seRoots.indexOf(comp) + 1;
if (index < seRoots.size()) { //
return seRoots.get(index);
}
}
}
}
return null;
}
private ComponentMatchCtx buildNextShadowSiblingCtx(ComponentMatchCtx ctx, ShadowElement se) {
ctx.moveToNextShadowSibling((Component) se);
//TODO need to match selectors
for (Selector selector : _selectorList) {
int i = selector.getSelectorIndex();
int posEnd = _posOffset > 0 ? _posOffset - 1 : 0;
int len = selector.size();
for (int j = len - 2; j >= posEnd; j--) {
Combinator cb = selector.getCombinator(j);
ComponentMatchCtx parent = ctx.getParent();
// ZK-2944: descendant and child combinator should have nothing to do with the previous matching status, clear it
if (cb == Selector.Combinator.DESCENDANT || cb == Selector.Combinator.CHILD) {
ctx.setQualified(i, j, false);
}
switch (cb) {
case DESCENDANT:
boolean parentPass = parent != null && parent.isQualified(i, j);
ctx.setQualified(i, j, parentPass && checkIdSpace(selector, j + 1, ctx));
if (parentPass && match(selector, ctx, j + 1))
ctx.setQualified(i, j + 1);
break;
case CHILD:
ctx.setQualified(i, j + 1,
parent != null && parent.isQualified(i, j) && match(selector, ctx, j + 1));
break;
case GENERAL_SIBLING:
if (ctx.isQualified(i, j))
ctx.setQualified(i, j + 1, match(selector, ctx, j + 1));
break;
case ADJACENT_SIBLING:
ctx.setQualified(i, j + 1, ctx.isQualified(i, j) && match(selector, ctx, j + 1));
ctx.setQualified(i, j, false);
}
}
}
if (_posOffset == 0)
matchLevel0(ctx);
return ctx;
}
private ComponentMatchCtx buildFirstShadowChildCtx(ComponentMatchCtx parent) {
ComponentMatchCtx ctx = new ComponentMatchCtx(
((HtmlShadowElement) ((ComponentCtrl) parent.getComponent()).getShadowRoots().get(0)), parent);
if (_posOffset == 0)
matchLevel0(ctx);
for (Selector selector : _selectorList) {
int i = selector.getSelectorIndex();
int posStart = _posOffset > 0 ? _posOffset - 1 : 0;
for (int j = posStart; j < selector.size() - 1; j++) {
switch (selector.getCombinator(j)) {
case CHILD:
if (parent.isQualified(i, j) && match(selector, ctx, j + 1))
ctx.setQualified(i, j + 1);
break;
}
}
}
return ctx;
}
private ComponentMatchCtx buildFirstChildCtx(ComponentMatchCtx parent) {
ComponentMatchCtx ctx = new ComponentMatchCtx(parent.getComponent().getFirstChild(), parent);
if (_posOffset == 0)
matchLevel0(ctx);
for (Selector selector : _selectorList) {
int i = selector.getSelectorIndex();
int posStart = _posOffset > 0 ? _posOffset - 1 : 0;
for (int j = posStart; j < selector.size() - 1; j++) {
switch (selector.getCombinator(j)) {
case DESCENDANT:
if (parent.isQualified(i, j) && checkIdSpace(selector, j + 1, ctx))
ctx.setQualified(i, j);
// no break
case CHILD:
if (parent.isQualified(i, j) && match(selector, ctx, j + 1))
ctx.setQualified(i, j + 1);
break;
}
}
}
//System.out.println(ctx); // TODO: debugger
return ctx;
}
private ComponentMatchCtx buildNextSiblingCtx(ComponentMatchCtx ctx) {
ctx.moveToNextSibling(); //no more status clearing when moving
for (Selector selector : _selectorList) {
int i = selector.getSelectorIndex();
int posEnd = _posOffset > 0 ? _posOffset - 1 : 0;
int len = selector.size();
// clear last position, may be overridden later
ctx.setQualified(i, len - 1, false);
for (int j = len - 2; j >= posEnd; j--) {
Combinator cb = selector.getCombinator(j);
ComponentMatchCtx parent = ctx.getParent();
// ZK-2944: descendant and child combinator should have nothing to do with the previous matching status, clear it
if (cb == Selector.Combinator.DESCENDANT || cb == Selector.Combinator.CHILD) {
ctx.setQualified(i, j, false);
}
switch (cb) {
case DESCENDANT:
boolean parentPass = parent != null && parent.isQualified(i, j);
ctx.setQualified(i, j, parentPass && checkIdSpace(selector, j + 1, ctx));
if (parentPass && match(selector, ctx, j + 1))
ctx.setQualified(i, j + 1);
break;
case CHILD:
ctx.setQualified(i, j + 1,
parent != null && parent.isQualified(i, j) && match(selector, ctx, j + 1));
break;
case GENERAL_SIBLING:
if (ctx.isQualified(i, j))
ctx.setQualified(i, j + 1, match(selector, ctx, j + 1));
break;
case ADJACENT_SIBLING:
ctx.setQualified(i, j + 1, ctx.isQualified(i, j) && match(selector, ctx, j + 1));
ctx.setQualified(i, j, false);
}
}
}
if (_posOffset == 0)
matchLevel0(ctx);
//System.out.println(ctx); // TODO: debugger
return ctx;
}
private static boolean checkIdSpace(Selector selector, int index, ComponentMatchCtx ctx) {
return !selector.requiresIdSpace(index) || !(ctx.getComponent() instanceof IdSpace);
}
private static boolean isDescendant(Component c1, Component c2) {
if (c1 == c2)
return true; // first c1 can be IdSpace
while ((c1 = c1.getParent()) != null) {
if (c1 == c2)
return true;
if (c1 instanceof IdSpace)
return c1 == c2;
}
return false;
}
private static boolean isGeneralSibling(Component c1, Component c2) {
while (c1 != null) {
if (c1 == c2)
return true;
c1 = c1.getPreviousSibling();
}
return false;
}
private void matchLevel0(ComponentMatchCtx ctx) {
for (Selector selector : _selectorList)
if (match(selector, ctx, 0))
ctx.setQualified(selector.getSelectorIndex(), 0);
}
private boolean match(Selector selector, ComponentMatchCtx ctx, int index) {
return ctx.match(selector.get(index), _localDefs);
}
}