zkbind/src/main/java/org/zkoss/bind/xel/zel/BindELContext.java

Summary

Maintainability
F
4 days
Test Coverage
/* BindELContext.java

    Purpose:
        
    Description:
        
    History:
        Aug 10, 2011 4:52:27 PM, Created by henrichen

Copyright (C) 2011 Potix Corporation. All Rights Reserved.
*/

package org.zkoss.bind.xel.zel;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.zkoss.bind.BindContext;
import org.zkoss.bind.Binder;
import org.zkoss.bind.Property;
import org.zkoss.bind.annotation.DependsOn;
import org.zkoss.bind.annotation.Immutable;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.bind.annotation.NotifyChangeDisabled;
import org.zkoss.bind.annotation.SmartNotifyChange;
import org.zkoss.bind.impl.AllocUtil;
import org.zkoss.bind.impl.BindContextUtil;
import org.zkoss.bind.impl.BinderImpl;
import org.zkoss.bind.impl.LoadChildrenBindingImpl;
import org.zkoss.bind.impl.LoadFormBindingImpl;
import org.zkoss.bind.impl.LoadPropertyBindingImpl;
import org.zkoss.bind.impl.PropertyImpl;
import org.zkoss.bind.init.ViewModelAnnotationResolvers;
import org.zkoss.bind.sys.BindEvaluatorX;
import org.zkoss.bind.sys.Binding;
import org.zkoss.lang.Primitives;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.xel.ExpressionX;
import org.zkoss.xel.ValueReference;
import org.zkoss.xel.XelContext;
import org.zkoss.xel.zel.XelELContext;
import org.zkoss.zel.ELResolver;
import org.zkoss.zel.impl.parser.AstBracketSuffix;
import org.zkoss.zel.impl.parser.AstDotSuffix;
import org.zkoss.zel.impl.parser.AstMethodParameters;
import org.zkoss.zel.impl.parser.AstValue;
import org.zkoss.zel.impl.parser.Node;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;

/**
 * ELContext for Binding.
 * @author henrichen
 * @since 6.0.0
 */
public class BindELContext extends XelELContext {
    public BindELContext(XelContext xelc) {
        super(xelc);
    }

    protected ELResolver newELResolver(XelContext xelc) {
        return new BindELResolver(xelc);
    }

    public Binding getBinding() {
        return (Binding) getXelContext().getAttribute(BinderImpl.BINDING); //see BindEvaluatorXImpl#newXelContext()
    }

    public BindContext getBindContext() {
        return (BindContext) getXelContext().getAttribute(BinderImpl.BINDCTX); //see BindEvaluatorXImpl#newXelContext()
    }

    public boolean ignoreTracker() {
        return getBinding() == null || Boolean.TRUE.equals(getXelContext().getAttribute(BinderImpl.IGNORE_TRACKER)); //see BindEvaluatorXImpl#newXelContext()
    }

    public Object getAttribute(String name) {
        return getXelContext().getAttribute(name); //see BindEvaluatorXImpl#newXelContext()
    }

    public Object setAttribute(String name, Object value) {
        return getXelContext().setAttribute(name, value);
    }

    /**
     * Removes the attribute, if any.
     * @since 8.0.0
     */
    public Object removeAttribute(String name) {
        return getXelContext().removeAttribute(name);
    }

    private static final String TMPBASE = "$TMPBASE$";

    public static Property prepareProperty(Object base, String prop, Object value, BindContext ctx) {
        if (ctx != null && prop.indexOf('[') >= 0) { //handle properties that containing [] indirect reference
            final Binder binder = ctx.getBinder();
            final Component comp = ctx.getComponent();
            Object old = null;
            try {
                old = comp.setAttribute(TMPBASE, base);
                final BindEvaluatorX eval = binder.getEvaluatorX();
                final BindContext bctx = BindContextUtil.newBindContext(binder, null, false, null, comp, null);
                final String expression = TMPBASE + (prop.startsWith("[") ? prop : ("." + prop));
                final ExpressionX exprX = eval.parseExpressionX(bctx, expression, Object.class);
                final String propTrim = prop.trim();
                final ValueReference valref = eval.getValueReference(bctx, comp, exprX);
                if (valref == null) {
                    throw new UiException(
                            "value reference not found by expression [" + exprX.getExpressionString() + "]");
                }
                base = valref.getBase();
                prop = propTrim.endsWith("]") ? "[" + valref.getProperty() + "]" : "" + valref.getProperty();
            } finally {
                comp.setAttribute(TMPBASE, old);
            }
        }
        return new PropertyImpl(base, prop, value);
    }

    //check method annotation and collect NotifyChange annotation
    public static Set<Property> getNotifys(Method m, Object base, String prop, Object value, BindContext ctx) {
        //TODO, Dennis, do we really need to pass value here?
        final Set<Property> notifys = new LinkedHashSet<Property>();
        if (m == null)
            return notifys;

        final NotifyChange annt = ViewModelAnnotationResolvers.getAnnotation(m, NotifyChange.class);
        //ZK-687 @NotifyChange should be doing automatically. 
        final NotifyChangeDisabled anntdis = ViewModelAnnotationResolvers.getAnnotation(m, NotifyChangeDisabled.class);

        final SmartNotifyChange sannt = ViewModelAnnotationResolvers.getAnnotation(m, SmartNotifyChange.class);

        if (annt != null && anntdis != null) {
            throw new UiException(
                    "don't use " + NotifyChange.class + " with " + NotifyChangeDisabled.class + ", choose only one");
        }
        if (sannt != null && anntdis != null) {
            throw new UiException("don't use " + SmartNotifyChange.class + " with " + NotifyChangeDisabled.class
                    + ", choose only one");
        }

        if (annt != null) {
            //if has annotation, use annotated value or prop (if no value in annotation)
            String[] notifies = annt.value();
            if (notifies.length > 0) {
                for (String notify : notifies) {
                    final Property propx = prepareProperty(base, notify, value, ctx);
                    notifys.add(propx);
                }
            } else if (prop != null) { //property is null in doExecute case
                notifys.add(new PropertyImpl(base, prop, value));
            }
        } else if (anntdis == null && prop != null) {
            notifys.add(new PropertyImpl(base, prop, value));
        }

        if (sannt != null) {
            //if has annotation, use annotated value or prop (if no value in annotation)
            String[] notifies = sannt.value();
            if (notifies.length > 0) {
                for (String notify : notifies) {
                    Property propx = null;
                    try {
                        if (!("*".equals(notify) || ".".equals(notify)))
                            propx = prepareProperty(base, notify, Fields.get(base, notify), ctx);
                        else
                            propx = prepareProperty(base, notify, value, ctx);
                    } catch (NoSuchMethodException e) {
                        propx = prepareProperty(base, notify, value, ctx); // eat the exception
                    }
                    if (propx != null)
                        notifys.add(propx);
                }
            } else if (prop != null) { //property is null in doExecute case
                notifys.add(new PropertyImpl(base, prop, value));
            }
        }

        return notifys;
    }
    
    @SuppressWarnings("unchecked")
    public static Set<Property> getNotifys(BindContext ctx) {
        return (Set<Property>) ctx.getAttribute(BinderImpl.NOTIFYS);
    }

    public static void addNotifys(Object base, String prop, Object value, BindContext ctx) {
        final Set<Property> properties = new HashSet<Property>(3);
        properties.add(new PropertyImpl(base, prop, value));
        addNotifys(properties, ctx);
    }

    public static void addNotifys(Method m, Object base, String prop, Object value, BindContext ctx) {
        final Set<Property> props = getNotifys(m, base, prop, value, ctx);
        addNotifys(props, ctx);
    }

    //utility method to add notifys to BindContext
    public static void addNotifys(Set<Property> props, BindContext ctx) {
        if (ctx == null) {
            return;
        }
        Set<Property> notifys = getNotifys(ctx);
        if (notifys == null) {
            notifys = new LinkedHashSet<Property>();
            ctx.setAttribute(BinderImpl.NOTIFYS, notifys);
        }
        notifys.addAll(props);
    }

    @SuppressWarnings("unchecked")
    private static Set<Property> getValidates(BindContext ctx) {
        return (Set<Property>) ctx.getAttribute(BinderImpl.VALIDATES);
    }

    //utility method to add validates to BindContext
    @SuppressWarnings("unused")
    private static void addValidates(Set<Property> props, BindContext ctx) {
        if (ctx == null) {
            return;
        }
        Set<Property> validates = getValidates(ctx);
        if (validates == null) {
            validates = new LinkedHashSet<Property>();
            ctx.setAttribute(BinderImpl.VALIDATES, validates);
        }
        validates.addAll(props);
    }

    public static String toNodeString(Node next, StringBuilder path) {
        if (next instanceof AstBracketSuffix) {
            final String bracketString = toNodeString(next.jjtGetChild(0), new StringBuilder()); //recursive
            path.append("[").append(bracketString).append("]");
        } else if (next instanceof AstValue) {
            for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
                final Node kid = next.jjtGetChild(j);
                toNodeString(kid, path); //recursive
            }
        } else if (next instanceof AstDotSuffix) {
            path.append(".").append(next.getImage());
        } else if (next instanceof AstMethodParameters) {
            StringBuilder subPath = new StringBuilder();
            for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
                if (j > 0)
                    subPath.append(',');
                final Node kid = next.jjtGetChild(j);
                toNodeString(kid, subPath); //recursive
            }
            path.append("(").append(subPath).append(")");
        } else {
            path.append(next.getImage());
        }
        return path.toString();
    }

    public static String toNodeString(Node next, StringBuffer path) {
        if (next instanceof AstBracketSuffix) {
            final String bracketString = toNodeString(next.jjtGetChild(0), new StringBuffer()); //recursive
            path.append("[").append(bracketString).append("]");
        } else if (next instanceof AstValue) {
            for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
                final Node kid = next.jjtGetChild(j);
                toNodeString(kid, path); //recursive
            }
        } else if (next instanceof AstDotSuffix) {
            path.append(".").append(next.getImage());
        } else if (next instanceof AstMethodParameters) {
            StringBuilder subPath = new StringBuilder();
            for (int j = 0, len = next.jjtGetNumChildren(); j < len; ++j) {
                if (j > 0)
                    subPath.append(',');
                final Node kid = next.jjtGetChild(j);
                toNodeString(kid, subPath); //recursive
            }
            path.append("(").append(subPath).append(")");
        } else {
            path.append(next.getImage());
        }
        return path.toString();
    }

    public static boolean isBracket(String script) {
        return script.startsWith("[") && script.endsWith("]");
    }

    public static String appendFields(String prefix, String field) {
        return prefix + (isBracket(field) ? "" : '.') + field;
    }

    //check method annotation and collect NotifyChange annotation
    public static void addDependsOnTrackings(Method m, String basepath, List<String> srcpath, Binding binding,
            BindContext ctx) {
        final DependsOn annt = ViewModelAnnotationResolvers.getAnnotation(m, DependsOn.class);
        if (annt != null) {
            String[] props = annt.value();
            if (props.length > 0) {
                if (binding instanceof LoadPropertyBindingImpl) {
                    ((LoadPropertyBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
                } else if (binding instanceof LoadFormBindingImpl) {
                    ((LoadFormBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
                } else if (binding instanceof LoadChildrenBindingImpl) {
                    ((LoadChildrenBindingImpl) binding).addDependsOnTrackings(srcpath, basepath, props);
                }
            }
        }
    }

    public static String pathToString(List<String> path) {
        final StringBuffer sb = new StringBuffer();
        for (String prop : path) {
            sb.append(prop);
        }
        return sb.toString();
    }

    /**
     * Prepare the dependsOn nodes
     * @param srcBinding associated binding of the source dependent field; e.g. <label id="full" value="@bind(vm.fullname)"/>
     * @param srcPath the source dependent field name series in list. e.g. "vm", "fullname" for "vm.fullname". 
     * @param dependsOnBasepath the base path for the depends-on field; e.g. the "vm" of the "vm.firstname"
     * @param dependsOnProp the property name of the depends-on field; e.g. the "firstname" of the "vm.firstname"
     */
    public static void addDependsOnTracking(Binding srcBinding, List<String> srcPath, String dependsOnBasepath,
            String dependsOnProp) {
        final String dependsOnPath = BindELContext.appendFields(dependsOnBasepath, dependsOnProp);
        final Component srcComp = srcBinding.getComponent();
        addDependsOnTracking(srcBinding, srcPath, srcComp, dependsOnPath, srcComp); //source component shared as DependsOnComp 
    }

    /**
     * Prepare the dependsOn nodes
     * @param srcBinding the binding with the source dependent field; e.g. <label id="full" value="@load(vm.fullname)"/>
     * @param srcPath the source dependent field name series in list; e.g. ["vm", "fullname"] for "vm.fullname".
     * @param srcComp the source component associated with the binding; e.g. <label>
     * @param dependsOnPath the depends-on property name series; e.g. "vm.firstname" 
     * @param dependsOnComp the depends-on component associated with the depends-on property name series binding; e.g. "vm.firstname"
     */
    public static void addDependsOnTracking(Binding srcBinding, List<String> srcPath, Component srcComp,
            String dependsOnPath, Component dependsOnComp) {
        final Binder binder = srcBinding.getBinder();
        final BindEvaluatorX eval = binder.getEvaluatorX();

        //parse depends on series
        BindContext ctxparse = BindContextUtil.newBindContext(binder, srcBinding, false, null, srcComp, null);
        ctxparse.setAttribute(BinderImpl.SRCPATH, srcPath);
        ctxparse.setAttribute(BinderImpl.DEPENDS_ON_COMP, dependsOnComp);
        ExpressionX expr = eval.parseExpressionX(ctxparse, dependsOnPath, Object.class); //prepare the tracking and association

        //bean association
        BindContext ctx = BindContextUtil.newBindContext(binder, srcBinding, false, null, srcComp, null);
        eval.getValue(ctx, srcComp, expr); //will call tieValue() and recursive back via BindELResolver
    }

    /** Returns whether the specified Object is an immutable object */
    public static boolean isImmutable(Object value) {
        //null is deemed as primitive
        if (value == null) {
            return true;
        }
        final Class<? extends Object> cls = value.getClass();
        return cls.isPrimitive() //value is primitive 
                || Primitives.toPrimitive(cls) != null //or a wrapper
                || value instanceof String //or a String
                || isAnnotatedImmutable(cls) //or an Immutable
                || cls.isEnum(); // value is enum class
    }

    public static String getModelName(Component comp) {
        return (String) AllocUtil.inst.processScript(BinderImpl.MODEL + comp.getUuid());
    }

    public static void addModel(Component comp, Object model) {
        final String name = getModelName(comp);
        comp.setAttribute(name, model);
    }

    public static Object removeModel(Component comp) {
        final String name = getModelName(comp);
        return comp.removeAttribute(name);
    }

    private static boolean isAnnotatedImmutable(Class<? extends Object> cls) {
        return cls.getAnnotation(Immutable.class) != null;
    }

    public void putContext(@SuppressWarnings("rawtypes") Class key, Object contextObject) {

        // Bug fixed for ZK-2787
        if (contextObject instanceof Method && key == Method.class) {
            BindContext bindCtx = (BindContext) getXelContext().getAttribute(BinderImpl.BINDCTX);
            if (bindCtx != null) {
                bindCtx.setAttribute(String.valueOf(key), ((Method) contextObject).getDeclaredAnnotations());
            }
        }
        super.putContext(key, contextObject);
    }
}