zk/src/main/java/org/zkoss/zk/ui/select/impl/ComponentLocalProperties.java

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * 
 */
package org.zkoss.zk.ui.select.impl;

import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.zkoss.lang.Objects;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.HtmlBasedComponent;
import org.zkoss.zk.ui.ShadowElement;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.sys.ComponentCtrl;

/**
 * A collection of utilities that check local properties of Components. 
 * Including type, ID, class, attribute, and pseudo class.
 * @since 6.0.0
 * @author simonpai
 */
public class ComponentLocalProperties {

    /**
     * Returns true if the selector matches the given component. Combinators 
     * are not allowed.
     * @param component
     * @param selector
     */
    public static boolean match(Component component, String selector) {
        return match(component, selector, null);
    }

    /**
     * Returns true if the selector matches the given component. Combinators 
     * are not allowed. 
     * @param component
     * @param selector
     * @param defs
     */
    public static boolean match(Component component, String selector, Map<String, PseudoClassDef> defs) {
        List<Selector> selectorList = new Parser().parse(selector);
        ComponentMatchCtx ctx = new ComponentMatchCtx(component, selectorList);
        for (Selector s : selectorList) {
            if (s.size() > 1)
                continue;
            if (match(ctx, s.get(0), defs))
                return true;
        }
        return false;
    }

    /*package*/ static boolean match(ComponentMatchCtx context, SimpleSelectorSequence seq,
            Map<String, PseudoClassDef> defs) {
        Component comp = context.getComponent();
        return matchType(comp, seq.getType()) && matchID(comp, seq.getId()) && matchClasses(comp, seq.getClasses())
                && matchAttributes(comp, seq.getAttributes())
                && matchPseudoClasses(context, seq.getPseudoClasses(), defs)
                && matchPseudoElements(comp, seq.getPseudoElements());
    }

    /*package*/ static boolean matchID(Component component, String id) {
        if (id == null)
            return true;
        return id.equals(component.getId());
    }

    /*package*/ static boolean matchType(Component component, String type) {
        if (type == null)
            return true;
        return component.getDefinition().getName().toLowerCase(java.util.Locale.ENGLISH)
                .equals(type.toLowerCase(java.util.Locale.ENGLISH));
    }

    /*package*/ static boolean matchClasses(Component component, Set<String> classes) {
        if (classes == null || classes.isEmpty())
            return true;

        if (!(component instanceof HtmlBasedComponent))
            return false;
        String scls = ((HtmlBasedComponent) component).getSclass();
        String zcls = ((HtmlBasedComponent) component).getZclass();

        for (String c : classes)
            if (scls == null || !scls.matches("(?:^|.*\\s)" + c + "(?:\\s.*|$)") && !Objects.equals(zcls, c))
                return false;

        return true;
    }

    /*package*/ static boolean matchAttributes(Component component, List<Attribute> attributes) {
        if (attributes == null || attributes.isEmpty())
            return true;

        for (Attribute attr : attributes)
            if (!matchValue(getValue(component, attr.getName()), attr))
                return false;

        return true;
    }

    /*package*/ static boolean matchPseudoClasses(Component component, List<PseudoClass> pseudoClasses,
            Map<String, PseudoClassDef> defs) {
        if (pseudoClasses == null || pseudoClasses.isEmpty())
            return true;
        return matchPseudoClasses(new ComponentMatchCtx(component), pseudoClasses, defs);
    }

    /*package*/ static boolean matchPseudoClasses(ComponentMatchCtx context, List<PseudoClass> pseudoClasses,
            Map<String, PseudoClassDef> defs) {
        if (pseudoClasses == null || pseudoClasses.isEmpty())
            return true;

        for (PseudoClass pc : pseudoClasses) {
            PseudoClassDef def = getPseudoClassDef(defs, pc.getName());
            if (def == null)
                throw new UiException("Pseudo class definition not found: " + pc.getName());

            String[] param = pc.getParameter();
            if (param == null ? !def.accept(context) : !def.accept(context, pc.getParameter()))
                return false;
        }
        return true;
    }

    private static boolean matchPseudoElements(Component comp, List<PseudoElement> pseudoElements) {
        if (pseudoElements == null || pseudoElements.isEmpty())
            return true;
        if (comp instanceof ShadowElement) {
            for (PseudoElement pe : pseudoElements) {
                if ("shadow".equals(pe.getName())) {
                    Component parent = ((ShadowElement) comp).getShadowHost();
                    if (parent != null) {
                        ComponentCtrl parentCtrl = (ComponentCtrl) parent;
                        if (parentCtrl.getShadowRoots().contains(comp)) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    // helper //
    private static Object getValue(Component component, String name) {
        try {
            return component.getClass().getMethod("get" + capitalize(name)).invoke(component);
        } catch (NoSuchMethodException e) {
            // no such method
        } catch (SecurityException e) {
            // SecurityManager doesn't like you
        } catch (IllegalAccessException e) {
            // attempted to call a non-public method
        } catch (InvocationTargetException e) {
            // exception thrown by the getter method
        }
        try {
            return component.getClass().getMethod("is" + capitalize(name)).invoke(component);
        } catch (NoSuchMethodException e) {
            // no such method
        } catch (SecurityException e) {
            // SecurityManager doesn't like you
        } catch (IllegalAccessException e) {
            // attempted to call a non-public method
        } catch (InvocationTargetException e) {
            // exception thrown by the getter method
        }
        // try dynamic attribute
        return component.getAttribute(name);
    }

    private static String capitalize(String str) {
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }

    private static PseudoClassDef getPseudoClassDef(Map<String, PseudoClassDef> defs, String className) {
        PseudoClassDef def = null;
        if (defs != null && !defs.isEmpty())
            def = defs.get(className);
        if (def != null)
            return def;
        return BasicPseudoClassDefs.getDefinition(className);
    }

    private static Object parseData(String source, Class<?> expectedType) {
        // TODO: enhance type support
        if (expectedType.equals(Integer.class))
            return new Integer(source);
        if (expectedType.equals(Boolean.class))
            return new Boolean(source);
        if (expectedType.equals(Double.class))
            return new Double(source);
        return source;
    }

    private static boolean matchValue(Object value, Attribute attr) {
        switch (attr.getOperator()) {
        case BEGIN_WITH:
            return value != null && value.toString().startsWith(attr.getValue());
        case END_WITH:
            return value != null && value.toString().endsWith(attr.getValue());
        case CONTAIN:
            return value != null && value.toString().contains(attr.getValue());
        case EQUAL:
        default:
            try {
                Object attrValue = parseData(attr.getValue(), attr.isQuoted() ? String.class : value.getClass());
                return Objects.equals(value, attrValue);
            } catch (Exception e) {
                // failed to convert attribute value to expected type
                return false;
            }
        }
    }

}