zel/src/main/java/org/zkoss/zel/BeanELResolver.java

Summary

Maintainability
F
6 days
Test Coverage
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.zkoss.zel;

import java.beans.FeatureDescriptor;
import java.beans.IndexedPropertyDescriptor;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import org.zkoss.zel.impl.util.ClassUtil;
import org.zkoss.zel.impl.util.ConcurrentCache;
import org.zkoss.zel.impl.util.ReflectionUtil;

public class BeanELResolver extends ELResolver {

    private static final int CACHE_SIZE;
    private static final String CACHE_SIZE_PROP =
        "org.zkoss.zel.BeanELResolver.CACHE_SIZE";

    static {
        if (System.getSecurityManager() == null) {
            CACHE_SIZE = Integer.parseInt(
                    System.getProperty(CACHE_SIZE_PROP, "1000"));
        } else {
            CACHE_SIZE = AccessController.doPrivileged(
                    new PrivilegedAction<Integer>() {

                    
                    public Integer run() {
                        return Integer.valueOf(
                                System.getProperty(CACHE_SIZE_PROP, "1000"));
                    }
                }).intValue();
        }
    }

    private final boolean readOnly;

    //This cache can be static - it allows reusing cache across multiple instances.
    private static final ConcurrentCache<String, BeanProperties> cache = new ConcurrentCache<String, BeanProperties>(CACHE_SIZE);

    // ZK-4546
    private static final ConcurrentCache<Class<?>, Map<CachedMethodInfo, Method>> METHODS_CACHE = new ConcurrentCache<Class<?>, Map<CachedMethodInfo, Method>>(CACHE_SIZE);

    public BeanELResolver() {
        this.readOnly = false;
    }

    public BeanELResolver(boolean readOnly) {
        this.readOnly = readOnly;
    }

    
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (base == null || property == null) {
            return null;
        }

        context.setPropertyResolved(base, property);
        return this.property(context, base, property).getPropertyType();
    }

    
    public Object getValue(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (base == null || property == null) {
            return null;
        }

        context.setPropertyResolved(base, property);
        Method m = this.property(context, base, property).read(context);
        try {
            final Object result = m.invoke(base, (Object[]) null);
            context.putContext(MethodInfo.class, base);
            context.putContext(Method.class, m);
            return result;
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            Util.handleThrowable(cause);
            throw new ELException(Util.message(context, "propertyReadError",
                    base.getClass().getName(), property.toString()), cause);
        } catch (Exception e) {
            throw new ELException(e);
        }
    }

    
    public void setValue(ELContext context, Object base, Object property,
            Object value) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (base == null || property == null) {
            return;
        }

        context.setPropertyResolved(base, property);

        if (this.readOnly) {
            throw new PropertyNotWritableException(Util.message(context,
                    "resolverNotWriteable", base.getClass().getName()));
        }

        Method m = this.property(context, base, property).write(context);
        

        
        try {
            m.invoke(base, value);
            context.putContext(MethodInfo.class, base);
            context.putContext(Method.class, m);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            Util.handleThrowable(cause);
            throw new ELException(Util.message(context, "propertyWriteError",
                    base.getClass().getName(), property.toString()), cause);
        } catch (IllegalArgumentException e) {
            //for ZK-1178: check type of mehtod's parameter is the same as type of value
            //XXX refactored into write() ?
            if (!checkType(m, value)) {
                Class<?> baseClass = base.getClass();
                for (Method method : ReflectionUtil.getSetter(baseClass, property.toString())) {
                    Class<?>[] clazzes = method.getParameterTypes();
                    if (ClassUtil.isInstance(value, clazzes[0])) {
                        m = method;
                        break;
                    }
                }
            }
            try {
                m.invoke(base, value);
                context.putContext(MethodInfo.class, base);
                context.putContext(Method.class, m);
            } catch (InvocationTargetException ee) {
                Throwable cause = ee.getCause();
                Util.handleThrowable(cause);
                throw new ELException(Util.message(context, "propertyWriteError",
                        base.getClass().getName(), property.toString()), cause);
            } catch (IllegalArgumentException ee) {
                throw new ELException(Util.message(context, "propertyWriteError",
                        new Object[] { base.getClass().getName(),
                                property.toString() }), ee);
            } catch (Exception ee) {
                throw new ELException(ee);
            }
            //// <=ZK-1178
        } catch (Exception e) {
            throw new ELException(e);
        }
    }
    
    private boolean checkType(Method m, Object value){
        Class<?>[] clazzes = m.getParameterTypes();
        if (clazzes.length!=1) { //not standard setter
            return false;
        }
        return clazzes[0].isInstance(value);
    }

    /**
     * @since EL 2.2
     */
    
    public Object invoke(ELContext context, Object base, Object method,
            Class<?>[] paramTypes, Object[] params) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (base == null || method == null) {
            return null;
        }

        ExpressionFactory factory = ExpressionFactory.newInstance();
        
        // Find the matching method
        Method matchingMethod = getMethod(base.getClass(), (String) factory.coerceToType(method, String.class), paramTypes, params);

        Class<?>[] parameterTypes = matchingMethod.getParameterTypes();
        Object[] parameters = null;
        if (parameterTypes.length > 0) {
            parameters = new Object[parameterTypes.length];
            @SuppressWarnings("null")  // params.length >= parameterTypes.length
            int paramCount = params.length;
            if (matchingMethod.isVarArgs()) {
                int varArgIndex = parameterTypes.length - 1;
                // First argCount-1 parameters are standard
                for (int i = 0; (i < varArgIndex); i++) {
                    parameters[i] = factory.coerceToType(params[i],
                            parameterTypes[i]);
                }
                // Last parameter is the varargs
                Class<?> varArgClass =
                    parameterTypes[varArgIndex].getComponentType();
                final Object varargs = Array.newInstance(
                    varArgClass,
                    (paramCount - varArgIndex));
                for (int i = (varArgIndex); i < paramCount; i++) {
                    Array.set(varargs, i - varArgIndex,
                            factory.coerceToType(params[i], varArgClass));
                }
                parameters[varArgIndex] = varargs;
                
            } else {
                parameters = new Object[parameterTypes.length];
                try {
                    for (int i = 0; i < parameterTypes.length; i++) {
                        parameters[i] = factory.coerceToType(params[i],
                                parameterTypes[i]);
                    }
                } catch (org.zkoss.zel.ELException ele) { 
                    //20110826, henrichen: could have located a method with wrong parameter types
                    if (paramTypes == null && ele.getCause() instanceof IllegalArgumentException) {
                        paramTypes = new Class<?>[parameters.length];
                        for (int i = 0; i < parameterTypes.length; i++) {
                            paramTypes[i] = params[i].getClass();
                        }
                        return invoke(context, base, method, paramTypes, params);
                    }
                    throw ele;
                }
            }
        }
        Object result = null;
        try {
            result = matchingMethod.invoke(base, parameters);
        } catch (IllegalArgumentException e) {
            throw new ELException(e);
        } catch (IllegalAccessException e) {
            throw new ELException(e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ThreadDeath) {
                throw (ThreadDeath) cause;
            }
            if (cause instanceof VirtualMachineError) {
                throw (VirtualMachineError) cause;
            }
            throw new ELException(cause);
        }
        
        context.setPropertyResolved(true);
        return result;  
    }

    
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (context == null) {
            throw new NullPointerException();
        }
        if (base == null || property == null) {
            return false;
        }

        context.setPropertyResolved(base, property);
        return this.readOnly || this.property(context, base, property).isReadOnly();
    }

    
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        if (base == null) {
            return null;
        }

        try {
            PropertyDescriptor[] pds = Util.getPropertyDescriptors(base.getClass());
            for (int i = 0; i < pds.length; i++) {
                //20110927, henrichen: Instrospector see getAbc(int) as IndexedPropertyDescriptor
                final PropertyDescriptor pd = pds[i]; 
                pd.setValue(RESOLVABLE_AT_DESIGN_TIME, Boolean.TRUE);
                if (pd instanceof IndexedPropertyDescriptor) {
                    pd.setValue(TYPE, ((IndexedPropertyDescriptor) pd).getIndexedPropertyType());
                } else {
                    pd.setValue(TYPE, pd.getPropertyType());
                }
            }
            return Arrays.asList((FeatureDescriptor[]) pds).iterator();
        } catch (IntrospectionException e) {
            //
        }

        return null;
    }

    
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        if (base != null) {
            return Object.class;
        }

        return null;
    }

    static final class BeanProperties {
        private final Map<String, BeanProperty> properties;

        private final Class<?> type;

        public BeanProperties(Class<?> type) throws ELException {
            this.type = type;
            this.properties = new HashMap<String, BeanProperty>();
            try {
                PropertyDescriptor[] pds = Util.getPropertyDescriptors(this.type);
                //20110927, henrichen: Introspector sees getAbc(int) as IndexedPropertyDescriptor
                //which might override getAbc() PropertyDescriptor; have to recover the case
                for (int i = 0; i < pds.length; i++) {
                    final PropertyDescriptor pd = recoverIndexedPropertyDescriptor(this.type, pds[i]);
                    this.properties.put(pd.getName(), new BeanProperty(type, pd));
                }
            } catch (IntrospectionException ie) {
                throw new ELException(ie);
            }
        }
        
        //20110927, henrichen: Introspector see getAbc(int) as IndexedPropertyDescriptor
        //which could merge away getAbc() PropertyDescriptor; have to recover this case
        //e.g. Map AbstractComponent#getAttributes() and Map AbstractComponent#getAttributes(int scope)
        private PropertyDescriptor recoverIndexedPropertyDescriptor(Class baseClz, PropertyDescriptor pd) {
            if (pd instanceof IndexedPropertyDescriptor) {
                final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) pd;
                if (ipd.getIndexedReadMethod() != null) {
                    try {
                        //try to get getter parameter type 
                        final String name = ipd.getName();
                        final Method rm = ipd.getIndexedReadMethod();
                        final String readMethodName = rm != null ? rm.getName() : null;
                        final Method wm = ipd.getIndexedWriteMethod();
                        final String writeMethodName = wm != null ? wm.getName() : null;
                        pd = new PropertyDescriptor(name, baseClz, readMethodName, writeMethodName);
                    } catch (IntrospectionException e) {
                        //ignore
                    } catch (SecurityException e) {
                        //ignore
                    }
                }
            }
            return pd;
        }

        private BeanProperty get(ELContext ctx, String name) {
            BeanProperty property = this.properties.get(name);
            if (property == null) {
                throw new PropertyNotFoundException(Util.message(ctx,
                        "propertyNotFound", type.getName(), name));
            }
            return property;
        }

        public BeanProperty getBeanProperty(String name) {
            return get(null, name);
        }

        private Class<?> getType() {
            return type;
        }
    }

    static final class BeanProperty {
        private final Class<?> type;

        private final Class<?> owner;

        private final PropertyDescriptor descriptor;

        private Method read;

        private Method write;

        public BeanProperty(Class<?> owner, PropertyDescriptor descriptor) {
            this.owner = owner;
            this.descriptor = descriptor;
            //20110927, henrichen: Introspector see getAbc(int) as IndexedPropertyDescriptor
            this.type = descriptor instanceof IndexedPropertyDescriptor ?  
                    ((IndexedPropertyDescriptor)descriptor).getIndexedPropertyType() :
                    descriptor.getPropertyType();
        }

        // Can't use Class<?> because API needs to match specification
        @SuppressWarnings("rawtypes")
        public Class getPropertyType() {
            return this.type;
        }

        public boolean isReadOnly() {
            return this.write == null &&
                    (null == (this.write = Util.getMethod(this.owner, descriptor.getWriteMethod())));
        }

        public Method getWriteMethod() {
            return write(null);
        }

        public Method getReadMethod() {
            return this.read(null);
        }

        private Method write(ELContext ctx) {
            if (this.write == null) {
                this.write = Util.getMethod(this.owner, descriptor.getWriteMethod());
                
                //20110921, henrichen: Introspector is too strict for Setter(must return void), here we loosen it
                if (this.write == null) {
                    final String name = this.descriptor.getName();
                    final String mname = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    //try to get setter parameter type 
                    final Class parameterTypes = this.descriptor instanceof IndexedPropertyDescriptor ?
                            ((IndexedPropertyDescriptor)this.descriptor).getIndexedPropertyType() :
                            this.descriptor.getPropertyType();
                            
                    try {
                        final Method m = this.owner.getMethod(mname, new Class[] {parameterTypes});
                        this.write = Util.getMethod(this.owner, m);
                    } catch (SecurityException e) {
                        //ignore
                    } catch (NoSuchMethodException e) {
                        //ignore
                    }
                    
                    //20120423, dennis: Introspector is too strict for Setter(must has same class argument as getter), 
                    //here we chosen a possible method by the getter type
                    if (this.write == null) {
                        try {
                            final Method m = ClassUtil.getCloseMethod(this.owner, mname, new Class[]{parameterTypes});
                            this.write = Util.getMethod(this.owner, m);
                        } catch (SecurityException e) {
                            //ignore
                        } catch (NoSuchMethodException e) {
                            //ignore
                        }
                    }
                }
                
                if (this.write == null) {
                    throw new PropertyNotWritableException(Util.message(ctx,
                            "propertyNotWritable", new Object[] {
                                    owner.getName(), descriptor.getName() }));
                }
            }
            return this.write;
        }

        private Method read(ELContext ctx) {
            if (this.read == null) {
                this.read = Util.getMethod(this.owner, descriptor.getReadMethod());
                if (this.read == null) {
                    throw new PropertyNotFoundException(Util.message(ctx,
                            "propertyNotReadable", new Object[] {
                                    owner.getName(), descriptor.getName() }));
                }
            }
            return this.read;
        }
    }

    private final BeanProperty property(ELContext ctx, Object base,
            Object property) {
        Class<?> type = base.getClass();
        String prop = property.toString();

        BeanProperties props = this.cache.get(type.getName());
        if (props == null || type != props.getType()) {
            props = new BeanProperties(type);
            this.cache.put(type.getName(), props);
        }

        return props.get(ctx, prop);
    }

    // ZK-4546
    private static final class CachedMethodInfo {
        private final String _methodName;
        private final Class<?>[] _paramTypes;

        public CachedMethodInfo(String methodName, Class<?>[] paramTypes) {
            this._methodName = methodName;
            this._paramTypes = paramTypes;
        }

        public String getMethodName() {
            return _methodName;
        }

        public Class<?>[] getParamTypes() {
            return _paramTypes;
        }

        @Override
        public int hashCode() {
            int result = 17;
            result = 31 * result + this._methodName.hashCode();
            result = 31 * result + (_paramTypes != null ? _paramTypes.length : 0);
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof CachedMethodInfo))
                return false;
            if (!((CachedMethodInfo) obj).getMethodName().equals(this._methodName))
                return false;
            Class<?>[] targetParamTypes = ((CachedMethodInfo) obj).getParamTypes();
            int len =  _paramTypes != null ? _paramTypes.length : 0;
            int len2 = targetParamTypes != null ? targetParamTypes.length : 0;
            if (len != len2)
                return false;
            for (int j = 0; j < len; ++j) {
                if (!Objects.equals(_paramTypes[j], targetParamTypes[j]))
                    return false;
            }
            return true;
        }
    }

    private static Method getMethod(Class<?> clazz, String methodName, Class<?>[] paramTypes, Object[] params) {
        Map<CachedMethodInfo, Method> clzMap = METHODS_CACHE.get(clazz);
        if (clzMap != null) {
            Method method = clzMap.get(new CachedMethodInfo(methodName, paramTypes));
            if (method != null)
                return method;
        }
        return getMethod0(clazz, methodName, paramTypes, params);
    }

    private static Method getMethod0(Class<?> clazz, String methodName, Class<?>[] paramTypes, Object[] params) {
        Method matchingMethod = null;
        if (paramTypes != null) {
            try {
                matchingMethod = Util.getMethod(clazz, clazz.getMethod(methodName, paramTypes));
            } catch (NoSuchMethodException e) {
                //throw new MethodNotFoundException(e);
                int paramCount = 0;
                if (params != null) {
                    paramCount = params.length;
                }
                Method[] methods = clazz.getMethods();
                for (Method m : methods) {
                    if (methodName.equals(m.getName())) {
                        if (m.getParameterTypes().length == paramCount) {
                            // Same number of parameters - use the first match
                            matchingMethod = Util.getMethod(clazz, m);
                            break;
                        }
                        if (m.isVarArgs()
                                && paramCount > m.getParameterTypes().length - 2) {
                            matchingMethod = Util.getMethod(clazz, m);
                        }
                    }
                }
                if (matchingMethod == null) {
                    throw new MethodNotFoundException("Unable to find method ["
                            + methodName + "] with [" + paramCount + "] parameters");
                }
            }
        } else {
            int paramCount = 0;
            if (params != null) {
                paramCount = params.length;
            }
            Method[] methods = clazz.getMethods();
            for (Method m : methods) {
                if (methodName.equals(m.getName())) {
                    if (m.getParameterTypes().length == paramCount) {
                        // Same number of parameters - use the first match
                        matchingMethod = Util.getMethod(clazz, m);
                        break;
                    }
                    if (m.isVarArgs()
                            && paramCount > m.getParameterTypes().length - 2) {
                        matchingMethod = Util.getMethod(clazz, m);
                    }
                }
            }
            if (matchingMethod == null) {
                throw new MethodNotFoundException("Unable to find method ["
                        + methodName + "] with [" + paramCount + "] parameters");
            }
        }
        if (matchingMethod != null)
            METHODS_CACHE.computeIfAbsent(clazz, k -> new ConcurrentHashMap<>())
                    .put(new CachedMethodInfo(methodName, paramTypes), matchingMethod);
        return matchingMethod;
    }
}