zkbind/src/main/java/org/zkoss/bind/impl/ParamCall.java

Summary

Maintainability
F
3 days
Test Coverage
package org.zkoss.bind.impl;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.bind.BindContext;
import org.zkoss.bind.Binder;
import org.zkoss.bind.Converter;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.BindingParams;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.bind.annotation.CookieParam;
import org.zkoss.bind.annotation.Default;
import org.zkoss.bind.annotation.ExecutionArgParam;
import org.zkoss.bind.annotation.ExecutionParam;
import org.zkoss.bind.annotation.HeaderParam;
import org.zkoss.bind.annotation.QueryParam;
import org.zkoss.bind.annotation.Scope;
import org.zkoss.bind.annotation.ScopeParam;
import org.zkoss.bind.annotation.SelectorParam;
import org.zkoss.bind.init.ViewModelAnnotationResolvers;
import org.zkoss.bind.paranamer.AdaptiveParanamer;
import org.zkoss.bind.paranamer.CachingParanamer;
import org.zkoss.bind.paranamer.Paranamer;
import org.zkoss.bind.sys.BindEvaluatorX;
import org.zkoss.bind.sys.ReferenceBinding;
import org.zkoss.json.JSONAware;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Strings;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.OperationException;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.metainfo.Parser;
import org.zkoss.zk.ui.select.Selectors;

/**
 * To help invoke a method with {@link BindingParam} etc.. features.
 * @author dennis
 * @since 6.0.0
 */
public class ParamCall {

    private static final Logger _log = LoggerFactory.getLogger(ParamCall.class);
    private static final Paranamer _PARANAMER = new CachingParanamer(new AdaptiveParanamer());

    protected Map<Class<? extends Annotation>, ParamResolver<Annotation>> _paramResolvers;
    private List<Type> _types; //to map class type directly, regardless the annotation
    private boolean _mappingType; //to enable the map class type without annotation, it is for compatible to rc2, only support BindeContext and Binder
    private ContextObjects _contextObjects;

    private static final String COOKIE_CACHE = "$PARAM_COOKIES$";
    public static final String BINDING_PARAM_CALL_TYPE = "org.zkoss.bind.BindingParamCall.type";

    private Component _root = null;
    private Component _component = null;
    private Execution _execution = null;
    private Binder _binder = null;
    private BindContext _bindContext = null;

    public ParamCall() {
        this(true);
    }

    public ParamCall(boolean mappingType) {
        _paramResolvers = new HashMap<Class<? extends Annotation>, ParamResolver<Annotation>>();
        _contextObjects = new ContextObjects();
        _types = new ArrayList<Type>();
        _mappingType = mappingType;
        _paramResolvers.put(ContextParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = _contextObjects.get(((ContextParam) anno).value());
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });
    }

    public void setBindContext(BindContext ctx) {
        _bindContext = ctx;
        _types.add(new Type(ctx.getClass(), _bindContext));
    }

    public BindContext getBindContext() {
        return _bindContext;
    }

    public void setBinder(Binder binder) {
        _binder = binder;
        _types.add(new Type(binder.getClass(), _binder));
        _root = binder.getView();
    }

    public Binder getBinder() {
        return _binder;
    }

    public void setBindingArgs(final Map<String, Object> bindingArgs) {
        this._bindingArgs = bindingArgs;
        _paramResolvers.put(BindingParam.class, new ParamResolver<Annotation>() {
            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = bindingArgs.get(getAnnotatedParameterName(BindingParam.class, ((BindingParam) anno).value(), parameterName));
                return resolveParameter0(val, returnType);
            }
        });
        _paramResolvers.put(BindingParams.class, new ParamResolver<Annotation>() {
            @Override
            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                try {
                    Object bean = Classes.newInstance(returnType, null);
                    BeanInfo beanInfo = Introspector.getBeanInfo(returnType);
                    for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
                        String propertyName = pd.getName();
                        Object propertyValue = bindingArgs.get(propertyName);
                        if (propertyValue != null || bindingArgs.containsKey(propertyName)) {
                            pd.getWriteMethod().invoke(
                                bean, Classes.coerce(pd.getPropertyType(), propertyValue)
                            );
                        }
                    }
                    return bean;
                } catch (Exception e) {
                    _log.error("", e);
                    throw UiException.Aide.wrap(e);
                }
            }
        });
    }

    protected String getAnnotatedParameterName(Class<? extends Annotation> annoClass,
                                             String annoValue,
                                             Supplier<String> parameterName) {
        if (!Strings.isEmpty(annoValue))
            return annoValue;

        String value = parameterName.get();
        if (!Strings.isEmpty(value))
            return value;

        throw new UiException("The parameter name can't be inferred. Please specify @" + annoClass.getSimpleName() + "(value) instead.");
    }

    public void call(Object base, Method method) {
        Method originalMethod = ViewModelAnnotationResolvers.getOriginalMethod(base, method);
        Class<?>[] paramTypes = originalMethod.getParameterTypes();
        java.lang.annotation.Annotation[][] parmAnnos = originalMethod.getParameterAnnotations();
        Object[] params = new Object[paramTypes.length];

        try {
            for (int i = 0; i < paramTypes.length; i++) {
                params[i] = resolveParameter(parmAnnos[i], paramTypes[i], originalMethod, i);
            }

            method.setAccessible(true); // Bug ZK-2428
            method.invoke(base, params);
        } catch (InvocationTargetException invokEx) {
            //Ian YT Tsai (2012.06.20), while InvocationTargetException,
            //using original exception is much meaningful.
            Throwable c = invokEx.getCause();
            if (c == null)
                c = invokEx;
            if (c instanceof OperationException) { //ZK-4255: Expectable exceptions
                _log.debug("", c);
            } else {
                _log.error("", c);
            }
            throw UiException.Aide.wrap(c);
        } catch (Exception e) {
            _log.error("", e);
            throw UiException.Aide.wrap(e);
        }
    }

    protected Object resolveParameter(Annotation[] parmAnnos, Class<?> paramType, Method method, int index) {
        Object val = null;
        boolean hitResolver = false;
        Default defAnno = null;
        for (Annotation anno : parmAnnos) {
            Class<?> annotype = anno.annotationType();

            if (defAnno == null && annotype.equals(Default.class)) {
                defAnno = (Default) anno;
                continue;
            }
            ParamResolver<Annotation> resolver = _paramResolvers.get(annotype);
            if (resolver == null)
                continue;
            hitResolver = true;
            val = resolver.resolveParameter(anno, paramType, () -> {
                String[] parameterNames = _PARANAMER.lookupParameterNames(method, false);
                return index < parameterNames.length ? parameterNames[index] : "";
            });
            if (val != null) {
                break;
            }
            //don't break until get a value
        }
        boolean isDefaultVal = false;
        if (val == null && defAnno != null) {
            isDefaultVal = true;
            val = Classes.coerce(paramType, defAnno.value());
        }

        //to compatible to rc2, do we have to?
        if (_mappingType && val == null && !hitResolver && _types != null) {
            for (Type type : _types) {
                if (type != null && paramType.isAssignableFrom(type.clz)) {
                    val = type.value;
                    isDefaultVal = false;
                    break;
                }
            }
        }

        if (val == null) {
            val = resolvePositionalOrNoAnnoParameter(paramType, method, index);
        } else if (isDefaultVal) { // from default
            Object positionalVal = resolvePositionalOrNoAnnoParameter(paramType, method, index);
            if (positionalVal != null)
                val = positionalVal;
        }

        return val;
    }

    protected Map<String, Object> _bindingArgs;

    private Object resolvePositionalOrNoAnnoParameter(Class<?> returnType, Method method, int index) {
        Object val = null;
        if (_bindingArgs != null) {
            int argIndex = 0;
            for (Map.Entry<String, Object> entry : _bindingArgs.entrySet()) {
                // skip using positional if the param key is not auto-generated
                if (argIndex == index) {
                    if (entry.getKey().startsWith(Parser.SIMPLIFIED_COMMAND_PARAM_PREFIX)) {
                        val = entry.getValue();
                    }
                    break;
                }
                argIndex++;
            }
            if (val == null) { // check param names
                String[] parameterNames = _PARANAMER.lookupParameterNames(method, false);
                if (index < parameterNames.length) {
                    val = _bindingArgs.get(parameterNames[index]);
                }
            }
            return resolveParameter0(val, returnType);
        }
        return null;
    }

    protected Object resolveParameter0(Object val, Class<?> returnType) {
        if (val != null && returnType.isAssignableFrom(val.getClass())) { //escape
            return val;
        } else if (Component.class.isAssignableFrom(returnType) && val instanceof String) {
            return _root.getDesktop().getComponentByUuidIfAny((String) val);
        } else if (val instanceof JSONAware) {
            BindContext bindContext = getBindContext();
            Binder binder = getBinder();
            @SuppressWarnings("rawtypes")
            Converter converter = binder.getConverter("jsonBindingParam");
            if (converter != null) {
                try {
                    bindContext.setAttribute(BINDING_PARAM_CALL_TYPE, returnType);
                    @SuppressWarnings("unchecked")
                    Object result = converter.coerceToBean(val, binder.getView(), bindContext);
                    return result;
                } catch (Exception ex) {
                    return Classes.coerce(returnType, val);
                } finally {
                    bindContext.setAttribute(BINDING_PARAM_CALL_TYPE, null);
                }
            } else
                return Classes.coerce(returnType, val);
        } else {
            return val == null ? null : Classes.coerce(returnType, val);
        }
    }

    //utility to hold implicit class and runtime value
    private static class Type {
        final Class<?> clz;
        final Object value;

        public Type(Class<?> clz, Object value) {
            this.clz = clz;
            this.value = value;
        }
    }

    public interface ParamResolver<T> {
        /**
         * @since 9.5.0
         */
        default Object resolveParameter(T anno, Class<?> returnType, Supplier<String> parameterName) {
            return null;
        }
    }

    public void setComponent(Component comp) {
        _component = comp;
        //scope param
        _paramResolvers.put(ScopeParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                final String name = getAnnotatedParameterName(ScopeParam.class, ((ScopeParam) anno).value(), parameterName);
                final Scope[] ss = ((ScopeParam) anno).scopes();

                Object val = null;

                for (Scope s : ss) {
                    switch (s) {
                    case AUTO:
                        if (ss.length == 1) {
                            if (_execution != null) val = _execution.getAttribute(name);
                            if (val == null) val = _component.getAttribute(name, true);
                        } else {
                            throw new UiException("don't use " + s + " with other scopes " + Arrays.toString(ss));
                        }
                    }
                }
                if (val == null) {
                    for (Scope scope : ss) {
                        final String scopeName = scope.getName();
                        Object scopeObj = Components.getImplicit(_component, scopeName);
                        if (scopeObj instanceof Map) {
                            val = ((Map<?, ?>) scopeObj).get(name);
                            if (val != null)
                                break;
                        } else if (scopeObj != null) {
                            _log.error("the scope of " + scopeName + " is not a Map, is " + scopeObj);
                        }
                    }
                }

                //zk-1469, 
                if (val instanceof ReferenceBinding) {
                    val = resolveReferenceBinding(name, (ReferenceBinding) val, returnType);
                }
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });

        //component
        _paramResolvers.put(SelectorParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                final String selector = getAnnotatedParameterName(SelectorParam.class, ((SelectorParam) anno).value(), parameterName);
                final List<Component> result = Selectors.find(_root, selector);
                Object val;
                if (!Collection.class.isAssignableFrom(returnType)) {
                    val = result.size() > 0 ? Classes.coerce(returnType, result.get(0)) : null;
                } else {
                    val = Classes.coerce(returnType, result);
                }
                return val;
            }
        });
    }

    private Object resolveReferenceBinding(String name, ReferenceBinding rbinding, Class<?> returnType) {
        BindEvaluatorX evalx = rbinding.getBinder().getEvaluatorX();
        //resolve by name or by rbinding.propertyString directly?
        Object val = BindEvaluatorXUtil.eval(evalx, rbinding.getComponent(), name, returnType, null);
        //following is quick but not safe because of the null arg
        //        val = ((ReferenceBinding)val).getValue(null);

        return val;
    }

    public void setExecution(Execution exec) {
        _execution = exec;
        //http param
        _paramResolvers.put(QueryParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = _execution.getParameter(getAnnotatedParameterName(QueryParam.class, ((QueryParam) anno).value(), parameterName));
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });
        _paramResolvers.put(HeaderParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = _execution.getHeader(getAnnotatedParameterName(HeaderParam.class, ((HeaderParam) anno).value(), parameterName));
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });
        _paramResolvers.put(CookieParam.class, new ParamResolver<Annotation>() {

            @SuppressWarnings("unchecked")
            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Map<String, Object> m = (Map<String, Object>) _execution.getAttribute(COOKIE_CACHE);
                if (m == null) {
                    final Object req = _execution.getNativeRequest();
                    m = new HashMap<String, Object>();
                    _execution.setAttribute(COOKIE_CACHE, m);

                    if (req instanceof HttpServletRequest) {
                        final Cookie[] cks = ((HttpServletRequest) req).getCookies();
                        if (cks != null) {
                            for (Cookie ck : cks) {
                                m.put(ck.getName().toLowerCase(java.util.Locale.ENGLISH), ck.getValue());
                            }
                        }
                    } else /* if(req instanceof PortletRequest)*/ {
                        //no cookie in protlet 1.0
                    }
                }
                Object val = m.get(getAnnotatedParameterName(CookieParam.class, ((CookieParam) anno).value(), parameterName).toLowerCase(java.util.Locale.ENGLISH));
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });

        //execution
        _paramResolvers.put(ExecutionParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = _execution.getAttribute(getAnnotatedParameterName(ExecutionParam.class, ((ExecutionParam) anno).value(), parameterName));
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });

        _paramResolvers.put(ExecutionArgParam.class, new ParamResolver<Annotation>() {

            public Object resolveParameter(Annotation anno, Class<?> returnType, Supplier<String> parameterName) {
                Object val = _execution.getArg().get(getAnnotatedParameterName(ExecutionArgParam.class, ((ExecutionArgParam) anno).value(), parameterName));
                return val == null ? null : Classes.coerce(returnType, val);
            }
        });
    }

    class ContextObjects {
        public Object get(ContextType type) {
            switch (type) {
            //bind contexts
            case BIND_CONTEXT:
                return _bindContext;
            case BINDER:
                return _binder;
            case COMMAND_NAME:
                return _bindContext == null ? null : _bindContext.getCommandName();
            case TRIGGER_EVENT:
                return _bindContext == null ? null : _bindContext.getTriggerEvent();
            //zk execution contexts
            case EXECUTION:
                return _execution;
            case COMPONENT:
                return _component;
            case SPACE_OWNER:
                return _component == null ? null : _component.getSpaceOwner();
            case VIEW:
                return _binder == null ? null : _binder.getView();
            case PAGE:
                return _component == null ? null : _component.getPage();
            case DESKTOP:
                return _component == null ? null : _component.getDesktop();
            case SESSION:
                return _component == null ? null : Components.getImplicit(_component, "session");
            case APPLICATION:
                return _component == null ? null : Components.getImplicit(_component, "application");
            }
            return null;
        }
    }
}