/*
 * 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.apache.mina.integration.jmx;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

import javax.management.Attribute;
import javax.management.AttributeChangeNotification;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeOperationsException;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBean;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;

import ognl.ExpressionSyntaxException;
import ognl.InappropriateExpressionException;
import ognl.NoSuchPropertyException;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.TypeConverter;

import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.filterchain.IoFilterChainBuilder;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.service.IoService;
import org.apache.mina.core.service.TransportMetadata;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.session.IoSessionDataStructureFactory;
import org.apache.mina.filter.executor.ExecutorFilter;
import org.apache.mina.integration.beans.CollectionEditor;
import org.apache.mina.integration.beans.ListEditor;
import org.apache.mina.integration.beans.MapEditor;
import org.apache.mina.integration.beans.PropertyEditorFactory;
import org.apache.mina.integration.beans.SetEditor;
import org.apache.mina.integration.ognl.IoFilterPropertyAccessor;
import org.apache.mina.integration.ognl.IoServicePropertyAccessor;
import org.apache.mina.integration.ognl.IoSessionPropertyAccessor;
import org.apache.mina.integration.ognl.PropertyTypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A {@link ModelMBean} wrapper implementation for a POJO.
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 *
 * @param <T> the type of the managed object
 */
public class ObjectMBean<T> implements ModelMBean, MBeanRegistration {

    private static final Map<ObjectName, Object> sources = new ConcurrentHashMap<>();

    /**
     * Get the monitored object
     * 
     * @param oname The object name
     * @return The monitored object
     */
    public static Object getSource(ObjectName oname) {
        return sources.get(oname);
    }

    static {
        OgnlRuntime.setPropertyAccessor(IoService.class, new IoServicePropertyAccessor());
        OgnlRuntime.setPropertyAccessor(IoSession.class, new IoSessionPropertyAccessor());
        OgnlRuntime.setPropertyAccessor(IoFilter.class, new IoFilterPropertyAccessor());
    }

    protected final static Logger LOGGER = LoggerFactory.getLogger(ObjectMBean.class);

    private final T source;

    private final TransportMetadata transportMetadata;

    private final MBeanInfo info;

    private final Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<>();

    private final TypeConverter typeConverter = new OgnlTypeConverter();

    private volatile MBeanServer server;

    private volatile ObjectName name;

    /**
     * Creates a new instance with the specified POJO.
     * 
     * @param source The original POJO
     */
    public ObjectMBean(T source) {
        if (source == null) {
            throw new IllegalArgumentException("source");
        }

        this.source = source;

        if (source instanceof IoService) {
            transportMetadata = ((IoService) source).getTransportMetadata();
        } else if (source instanceof IoSession) {
            transportMetadata = ((IoSession) source).getTransportMetadata();
        } else {
            transportMetadata = null;
        }

        this.info = createModelMBeanInfo(source);
    }

    public final Object getAttribute(String fqan) throws AttributeNotFoundException, MBeanException,
    ReflectionException {
        try {
            return convertValue(source.getClass(), fqan, getAttribute0(fqan), false);
        } catch (AttributeNotFoundException e) {
            // Do nothing
        } catch (Exception e) {
            throwMBeanException(e);
        }

        // Check if the attribute exist, if not throw an exception
        PropertyDescriptor pdesc = propertyDescriptors.get(fqan);
        if (pdesc == null) {
            throwMBeanException(new IllegalArgumentException("Unknown attribute: " + fqan));
        }

        try {

            Object parent = getParent(fqan);
            boolean writable = isWritable(source.getClass(), pdesc);

            return convertValue(parent.getClass(), getLeafAttributeName(fqan),
                    getAttribute(source, fqan, pdesc.getPropertyType()), writable);
        } catch (Exception e) {
            throwMBeanException(e);
        }

        throw new IllegalStateException();
    }

    public final void setAttribute(Attribute attribute) throws AttributeNotFoundException, MBeanException,
    ReflectionException {
        String aname = attribute.getName();
        Object avalue = attribute.getValue();

        try {
            setAttribute0(aname, avalue);
        } catch (AttributeNotFoundException e) {
            // Do nothing
        } catch (Exception e) {
            throwMBeanException(e);
        }

        PropertyDescriptor pdesc = propertyDescriptors.get(aname);
        if (pdesc == null) {
            throwMBeanException(new IllegalArgumentException("Unknown attribute: " + aname));
        }

        try {
            PropertyEditor e = getPropertyEditor(getParent(aname).getClass(), pdesc.getName(), pdesc.getPropertyType());
            e.setAsText((String) avalue);
            OgnlContext ctx = (OgnlContext) Ognl.createDefaultContext(source);
            ctx.setTypeConverter(typeConverter);
            Ognl.setValue(aname, ctx, source, e.getValue());
        } catch (Exception e) {
            throwMBeanException(e);
        }
    }

    public final Object invoke(String name, Object params[], String signature[]) throws MBeanException,
    ReflectionException {

        // Handle synthetic operations first.
        if (name.equals("unregisterMBean")) {
            try {
                server.unregisterMBean(this.name);
                return null;
            } catch (InstanceNotFoundException e) {
                throwMBeanException(e);
            }
        }

        try {
            return convertValue(null, null, invoke0(name, params, signature), false);
        } catch (NoSuchMethodException e) {
            // Do nothing
        } catch (Exception e) {
            throwMBeanException(e);
        }

        // And then try reflection.
        Class<?>[] paramTypes = new Class[signature.length];
        for (int i = 0; i < paramTypes.length; i++) {
            try {
                paramTypes[i] = getAttributeClass(signature[i]);
            } catch (ClassNotFoundException e) {
                throwMBeanException(e);
            }

            PropertyEditor e = getPropertyEditor(source.getClass(), "p" + i, paramTypes[i]);
            if (e == null) {
                throwMBeanException(new IllegalArgumentException("Conversion failure: " + params[i]));
            }

            e.setValue(params[i]);
            params[i] = e.getAsText();
        }

        try {
            // Find the right method.
            for (Method m : source.getClass().getMethods()) {
                if (!m.getName().equalsIgnoreCase(name)) {
                    continue;
                }
                Class<?>[] methodParamTypes = m.getParameterTypes();
                if (methodParamTypes.length != params.length) {
                    continue;
                }

                Object[] convertedParams = new Object[params.length];
                for (int i = 0; i < params.length; i++) {
                    if (Iterable.class.isAssignableFrom(methodParamTypes[i])) {
                        // Generics are not supported.
                        convertedParams = null;
                        break;
                    }
                    PropertyEditor e = getPropertyEditor(source.getClass(), "p" + i, methodParamTypes[i]);
                    if (e == null) {
                        convertedParams = null;
                        break;
                    }

                    e.setAsText((String) params[i]);
                    convertedParams[i] = e.getValue();
                }
                if (convertedParams == null) {
                    continue;
                }

                return convertValue(m.getReturnType(), "returnValue", m.invoke(source, convertedParams), false);
            }

            // No methods matched.
            throw new IllegalArgumentException("Failed to find a matching operation: " + name);
        } catch (Exception e) {
            throwMBeanException(e);
        }

        throw new IllegalStateException();
    }

    /**
     * @return The monitored object
     */
    public final T getSource() {
        return source;
    }

    /**
     * @return The MBrean server
     */
    public final MBeanServer getServer() {
        return server;
    }

    /**
     * @return The monitored object name
     */
    public final ObjectName getName() {
        return name;
    }

    public final MBeanInfo getMBeanInfo() {
        return info;
    }

    public final AttributeList getAttributes(String names[]) {
        AttributeList answer = new AttributeList();
        for (int i = 0; i < names.length; i++) {
            try {
                answer.add(new Attribute(names[i], getAttribute(names[i])));
            } catch (Exception e) {
                // Ignore.
            }
        }
        return answer;
    }

    public final AttributeList setAttributes(AttributeList attributes) {
        // Prepare and return our response, eating all exceptions
        String names[] = new String[attributes.size()];
        int n = 0;
        Iterator<Object> items = attributes.iterator();
        while (items.hasNext()) {
            Attribute item = (Attribute) items.next();
            names[n++] = item.getName();
            try {
                setAttribute(item);
            } catch (Exception e) {
                // Ignore all exceptions
            }
        }

        return getAttributes(names);
    }

    public final void setManagedResource(Object resource, String type) throws InstanceNotFoundException,
    InvalidTargetObjectTypeException, MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());

    }

    public final void setModelMBeanInfo(ModelMBeanInfo info) throws MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    @Override
    public final String toString() {
        return (source == null ? "" : source.toString());
    }

    public void addAttributeChangeNotificationListener(NotificationListener listener, String name, Object handback) {
        // Do nothing
    }

    public void removeAttributeChangeNotificationListener(NotificationListener listener, String name)
            throws ListenerNotFoundException {
        // Do nothing
    }

    public void sendAttributeChangeNotification(AttributeChangeNotification notification) throws MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    public void sendAttributeChangeNotification(Attribute oldValue, Attribute newValue) throws MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    public void sendNotification(Notification notification) throws MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    public void sendNotification(String message) throws MBeanException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());

    }

    public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback)
            throws IllegalArgumentException {
        // Do nothing
    }

    public MBeanNotificationInfo[] getNotificationInfo() {
        return new MBeanNotificationInfo[0];
    }

    public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
        // Do nothing
    }

    public void load() throws InstanceNotFoundException, MBeanException, RuntimeOperationsException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    public void store() throws InstanceNotFoundException, MBeanException, RuntimeOperationsException {
        throw new RuntimeOperationsException(new UnsupportedOperationException());
    }

    public final ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception {
        this.server = server;
        this.name = name;
        return name;
    }

    public final void postRegister(Boolean registrationDone) {
        if (registrationDone) {
            sources.put(name, source);
        }
    }

    public final void preDeregister() throws Exception {
        // Do nothing
    }

    public final void postDeregister() {
        sources.remove(name);
        this.server = null;
        this.name = null;
    }

    private MBeanInfo createModelMBeanInfo(T source) {
        String className = source.getClass().getName();
        String description = "";

        ModelMBeanConstructorInfo[] constructors = new ModelMBeanConstructorInfo[0];
        ModelMBeanNotificationInfo[] notifications = new ModelMBeanNotificationInfo[0];

        List<ModelMBeanAttributeInfo> attributes = new ArrayList<>();
        List<ModelMBeanOperationInfo> operations = new ArrayList<>();

        addAttributes(attributes, source);
        addExtraAttributes(attributes);

        addOperations(operations, source);
        addExtraOperations(operations);
        operations.add(new ModelMBeanOperationInfo("unregisterMBean", "unregisterMBean", new MBeanParameterInfo[0],
                void.class.getName(), ModelMBeanOperationInfo.ACTION));

        return new ModelMBeanInfoSupport(className, description,
                attributes.toArray(new ModelMBeanAttributeInfo[attributes.size()]), constructors,
                operations.toArray(new ModelMBeanOperationInfo[operations.size()]), notifications);
    }

    private void addAttributes(List<ModelMBeanAttributeInfo> attributes, Object object) {
        addAttributes(attributes, object, object.getClass(), "");
    }

    private void addAttributes(List<ModelMBeanAttributeInfo> attributes, Object object, Class<?> type, String prefix) {

        PropertyDescriptor[] pdescs;
        try {
            pdescs = Introspector.getBeanInfo(type).getPropertyDescriptors();
        } catch (IntrospectionException e) {
            return;
        }

        for (PropertyDescriptor pdesc : pdescs) {
            // Ignore a write-only property.
            if (pdesc.getReadMethod() == null) {
                continue;
            }

            // Ignore unmanageable property.
            String attrName = pdesc.getName();
            Class<?> attrType = pdesc.getPropertyType();
            if (attrName.equals("class")) {
                continue;
            }
            if (!isReadable(type, attrName)) {
                continue;
            }

            // Expand if possible.
            if (isExpandable(type, attrName)) {
                expandAttribute(attributes, object, prefix, pdesc);
                continue;
            }

            // Ordinary property.
            String fqan = prefix + attrName;
            boolean writable = isWritable(type, pdesc);
            attributes.add(new ModelMBeanAttributeInfo(fqan, convertType(object.getClass(), attrName, attrType,
                    writable).getName(), pdesc.getShortDescription(), true, writable, false));

            propertyDescriptors.put(fqan, pdesc);
        }
    }

    private boolean isWritable(Class<?> type, PropertyDescriptor pdesc) {
        if (type == null) {
            throw new IllegalArgumentException("type");
        }
        if (pdesc == null) {
            return false;
        }
        String attrName = pdesc.getName();
        Class<?> attrType = pdesc.getPropertyType();
        boolean writable = (pdesc.getWriteMethod() != null) || isWritable(type, attrName);
        if (getPropertyEditor(type, attrName, attrType) == null) {
            writable = false;
        }
        return writable;
    }

    private void expandAttribute(List<ModelMBeanAttributeInfo> attributes, Object object, String prefix,
            PropertyDescriptor pdesc) {
        Object property;
        String attrName = pdesc.getName();
        try {
            property = getAttribute(object, attrName, pdesc.getPropertyType());
        } catch (Exception e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Unexpected exception.", e);
            }
            
            return;
        }

        if (property == null) {
            return;
        }

        addAttributes(attributes, property, property.getClass(), prefix + attrName + '.');
    }

    private void addOperations(List<ModelMBeanOperationInfo> operations, Object object) {

        for (Method m : object.getClass().getMethods()) {
            String mname = m.getName();

            // Ignore getters and setters.
            if (mname.startsWith("is") || mname.startsWith("get") || mname.startsWith("set")) {
                continue;
            }

            // Ignore Object methods.
            if (mname.matches("(wait|notify|notifyAll|toString|equals|compareTo|hashCode|clone)")) {
                continue;
            }

            // Ignore other user-defined non-operations.
            if (!isOperation(mname, m.getParameterTypes())) {
                continue;
            }

            List<MBeanParameterInfo> signature = new ArrayList<>();
            int i = 1;
            for (Class<?> paramType : m.getParameterTypes()) {
                String paramName = "p" + (i++);
                if (getPropertyEditor(source.getClass(), paramName, paramType) == null) {
                    continue;
                }
                signature.add(new MBeanParameterInfo(paramName, convertType(null, null, paramType, true).getName(),
                        paramName));
            }

            Class<?> returnType = convertType(null, null, m.getReturnType(), false);
            operations.add(new ModelMBeanOperationInfo(m.getName(), m.getName(), signature
                    .toArray(new MBeanParameterInfo[signature.size()]), returnType.getName(),
                    ModelMBeanOperationInfo.ACTION));
        }
    }

    private Object getParent(String fqan) throws OgnlException {
        Object parent;
        int dotIndex = fqan.lastIndexOf('.');
        if (dotIndex < 0) {
            parent = source;
        } else {
            parent = getAttribute(source, fqan.substring(0, dotIndex), null);
        }
        return parent;
    }

    private String getLeafAttributeName(String fqan) {
        int dotIndex = fqan.lastIndexOf('.');
        if (dotIndex < 0) {
            return fqan;
        }
        return fqan.substring(dotIndex + 1);
    }

    private Class<?> getAttributeClass(String signature) throws ClassNotFoundException {
        if (signature.equals(Boolean.TYPE.getName())) {
            return Boolean.TYPE;
        }
        if (signature.equals(Byte.TYPE.getName())) {
            return Byte.TYPE;
        }
        if (signature.equals(Character.TYPE.getName())) {
            return Character.TYPE;
        }
        if (signature.equals(Double.TYPE.getName())) {
            return Double.TYPE;
        }
        if (signature.equals(Float.TYPE.getName())) {
            return Float.TYPE;
        }
        if (signature.equals(Integer.TYPE.getName())) {
            return Integer.TYPE;
        }
        if (signature.equals(Long.TYPE.getName())) {
            return Long.TYPE;
        }
        if (signature.equals(Short.TYPE.getName())) {
            return Short.TYPE;
        }

        try {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            if (cl != null) {
                return cl.loadClass(signature);
            }
        } catch (ClassNotFoundException e) {
            // Do nothing
        }

        return Class.forName(signature);
    }

    private Object getAttribute(Object object, String fqan, Class<?> attrType) throws OgnlException {
        Object property;
        OgnlContext ctx = (OgnlContext) Ognl.createDefaultContext(object);
        ctx.setTypeConverter(new OgnlTypeConverter());
        if (attrType == null) {
            property = Ognl.getValue(fqan, ctx, object);
        } else {
            property = Ognl.getValue(fqan, ctx, object, attrType);
        }
        return property;
    }

    private Class<?> convertType(Class<?> type, String attrName, Class<?> attrType, boolean writable) {
        if ((attrName != null) && ((attrType == Long.class) || (attrType == long.class))) {
            if (attrName.endsWith("Time") && (attrName.indexOf("Total") < 0) && (attrName.indexOf("Min") < 0)
                    && (attrName.indexOf("Max") < 0) && (attrName.indexOf("Avg") < 0)
                    && (attrName.indexOf("Average") < 0) && !propertyDescriptors.containsKey(attrName + "InMillis")) {
                return Date.class;
            }
        }

        if (IoFilterChain.class.isAssignableFrom(attrType)) {
            return Map.class;
        }

        if (IoFilterChainBuilder.class.isAssignableFrom(attrType)) {
            return Map.class;
        }

        if (!writable) {
            if (Collection.class.isAssignableFrom(attrType) || Map.class.isAssignableFrom(attrType)) {
                if (List.class.isAssignableFrom(attrType)) {
                    return List.class;
                }
                if (Set.class.isAssignableFrom(attrType)) {
                    return Set.class;
                }
                if (Map.class.isAssignableFrom(attrType)) {
                    return Map.class;
                }
                return Collection.class;
            }

            if (attrType.isPrimitive() || Date.class.isAssignableFrom(attrType)
                    || Boolean.class.isAssignableFrom(attrType) || Character.class.isAssignableFrom(attrType)
                    || Number.class.isAssignableFrom(attrType)) {
                if ((attrName == null) || !attrName.endsWith("InMillis")
                        || !propertyDescriptors.containsKey(attrName.substring(0, attrName.length() - 8))) {
                    return attrType;
                }
            }
        }

        return String.class;
    }

    private Object convertValue(Class<?> type, String attrName, Object v, boolean writable) {
        if (v == null) {
            return null;
        }

        if ((attrName != null) && (v instanceof Long)) {
            if (attrName.endsWith("Time") && (attrName.indexOf("Total") < 0) && (attrName.indexOf("Min") < 0)
                    && (attrName.indexOf("Max") < 0) && (attrName.indexOf("Avg") < 0)
                    && (attrName.indexOf("Average") < 0) && !propertyDescriptors.containsKey(attrName + "InMillis")) {
                long time = (Long) v;
                if (time <= 0) {
                    return null;
                }

                return new Date((Long) v);
            }
        }

        if ((v instanceof IoSessionDataStructureFactory) || (v instanceof IoHandler)) {
            return v.getClass().getName();
        }

        if (v instanceof IoFilterChainBuilder) {
            Map<String, String> filterMapping = new LinkedHashMap<>();
            if (v instanceof DefaultIoFilterChainBuilder) {
                for (IoFilterChain.Entry e : ((DefaultIoFilterChainBuilder) v).getAll()) {
                    filterMapping.put(e.getName(), e.getFilter().getClass().getName());
                }
            } else {
                filterMapping.put("Unknown builder type", v.getClass().getName());
            }
            return filterMapping;
        }

        if (v instanceof IoFilterChain) {
            Map<String, String> filterMapping = new LinkedHashMap<>();
            for (IoFilterChain.Entry e : ((IoFilterChain) v).getAll()) {
                filterMapping.put(e.getName(), e.getFilter().getClass().getName());
            }
            return filterMapping;
        }

        if (!writable) {
            if ((v instanceof Collection) || (v instanceof Map)) {
                if (v instanceof List) {
                    return convertCollection(v, new ArrayList<>());
                }
                if (v instanceof Set) {
                    return convertCollection(v, new LinkedHashSet<>());
                }
                if (v instanceof Map) {
                    return convertCollection(v, new LinkedHashMap<>());
                }
                return convertCollection(v, new ArrayList<>());
            }

            if ((v instanceof Date) || (v instanceof Boolean) || (v instanceof Character) || (v instanceof Number)) {
                if ((attrName == null) || !attrName.endsWith("InMillis")
                        || !propertyDescriptors.containsKey(attrName.substring(0, attrName.length() - 8))) {
                    return v;
                }
            }
        }

        PropertyEditor editor = getPropertyEditor(type, attrName, v.getClass());
        if (editor != null) {
            editor.setValue(v);
            return editor.getAsText();
        }

        return v.toString();
    }

    private Object convertCollection(Object src, Collection<Object> dst) {
        Collection<?> srcCol = (Collection<?>) src;
        for (Object e : srcCol) {
            Object convertedValue = convertValue(dst.getClass(), "element", e, false);
            if ((e != null) && (convertedValue == null)) {
                convertedValue = e.toString();
            }
            dst.add(convertedValue);
        }
        return dst;
    }

    private Object convertCollection(Object src, Map<Object, Object> dst) {
        Map<?, ?> srcCol = (Map<?, ?>) src;
        for (Map.Entry<?, ?> e : srcCol.entrySet()) {
            Object convertedKey = convertValue(dst.getClass(), "key", e.getKey(), false);
            Object convertedValue = convertValue(dst.getClass(), "value", e.getValue(), false);
            if ((e.getKey() != null) && (convertedKey == null)) {
                convertedKey = e.getKey().toString();
            }
            if ((e.getValue() != null) && (convertedValue == null)) {
                convertedKey = e.getValue().toString();
            }
            dst.put(convertedKey, convertedValue);
        }
        return dst;
    }

    private void throwMBeanException(Throwable e) throws MBeanException {
        if (e instanceof OgnlException) {
            OgnlException ognle = (OgnlException) e;

            if (ognle.getReason() != null) {
                throwMBeanException(ognle.getReason());
            } else {
                String message = ognle.getMessage();

                if (e instanceof NoSuchPropertyException) {
                    message = "No such property: " + message;
                } else if (e instanceof ExpressionSyntaxException) {
                    message = "Illegal expression syntax: " + message;
                } else if (e instanceof InappropriateExpressionException) {
                    message = "Inappropriate expression: " + message;
                }

                e = new IllegalArgumentException(message);
                e.setStackTrace(ognle.getStackTrace());
            }
        }
        if (e instanceof InvocationTargetException) {
            throwMBeanException(e.getCause());
        }

        LOGGER.warn("Unexpected exception.", e);
        if (e.getClass().getPackage().getName().matches("javax?\\..+")) {
            if (e instanceof Exception) {
                throw new MBeanException((Exception) e, e.getMessage());
            }

            throw new MBeanException(new RuntimeException(e), e.getMessage());
        }

        throw new MBeanException(new RuntimeException(e.getClass().getName() + ": " + e.getMessage()), e.getMessage());
    }

    protected Object getAttribute0(String fqan) throws Exception {
        throw new AttributeNotFoundException(fqan);
    }

    protected void setAttribute0(String attrName, Object attrValue) throws Exception {
        throw new AttributeNotFoundException(attrName);
    }

    protected Object invoke0(String name, Object params[], String signature[]) throws Exception {
        throw new NoSuchMethodException();
    }

    protected boolean isReadable(Class<?> type, String attrName) {
        if (IoService.class.isAssignableFrom(type) && attrName.equals("filterChain")) {
            return false;
        }
        if (IoService.class.isAssignableFrom(type) && attrName.equals("localAddress")) {
            return false;
        }
        if (IoService.class.isAssignableFrom(type) && attrName.equals("defaultLocalAddress")) {
            return false;
        }
        if (IoSession.class.isAssignableFrom(type) && attrName.equals("attachment")) {
            return false;
        }
        if (IoSession.class.isAssignableFrom(type) && attrName.equals("attributeKeys")) {
            return false;
        }
        if (IoSession.class.isAssignableFrom(type) && attrName.equals("closeFuture")) {
            return false;
        }

        if (ThreadPoolExecutor.class.isAssignableFrom(type) && attrName.equals("queue")) {
            return false;
        }

        return true;
    }

    protected boolean isWritable(Class<?> type, String attrName) {
        if (IoService.class.isAssignableFrom(type) && attrName.startsWith("defaultLocalAddress")) {
            return true;
        }
        return false;
    }

    protected Class<?> getElementType(Class<?> type, String attrName) {
        if ((transportMetadata != null) && IoAcceptor.class.isAssignableFrom(type)
                && "defaultLocalAddresses".equals(attrName)) {
            return transportMetadata.getAddressType();
        }
        return String.class;
    }

    protected Class<?> getMapKeyType(Class<?> type, String attrName) {
        return String.class;
    }

    protected Class<?> getMapValueType(Class<?> type, String attrName) {
        return String.class;
    }

    protected boolean isExpandable(Class<?> type, String attrName) {

        if (IoService.class.isAssignableFrom(type)) {
            if (attrName.equals("statistics") || attrName.equals("sessionConfig")
                    || attrName.equals("transportMetadata") || attrName.equals("config")
                    || attrName.equals("transportMetadata")) {
                return true;
            }
        }

        if (ExecutorFilter.class.isAssignableFrom(type) && attrName.equals("executor")) {
            return true;
        }

        if (ThreadPoolExecutor.class.isAssignableFrom(type) && attrName.equals("queueHandler")) {
            return true;
        }

        return false;
    }

    protected boolean isOperation(String methodName, Class<?>[] paramTypes) {
        return true;
    }

    protected void addExtraAttributes(List<ModelMBeanAttributeInfo> attributes) {
        // Do nothing
    }

    protected void addExtraOperations(List<ModelMBeanOperationInfo> operations) {
        // Do nothing
    }

    protected PropertyEditor getPropertyEditor(Class<?> type, String attrName, Class<?> attrType) {
        if (type == null) {
            throw new IllegalArgumentException("type");
        }

        if (attrName == null) {
            throw new IllegalArgumentException("attrName");
        }

        if ((transportMetadata != null) && (attrType == SocketAddress.class)) {
            attrType = transportMetadata.getAddressType();
        }

        if (((attrType == Long.class) || (attrType == long.class))) {
            if (attrName.endsWith("Time") && (attrName.indexOf("Total") < 0) && (attrName.indexOf("Min") < 0)
                    && (attrName.indexOf("Max") < 0) && (attrName.indexOf("Avg") < 0)
                    && (attrName.indexOf("Average") < 0) && !propertyDescriptors.containsKey(attrName + "InMillis")) {
                return PropertyEditorFactory.getInstance(Date.class);
            }

            if (attrName.equals("id")) {
                return PropertyEditorFactory.getInstance(String.class);
            }
        }

        if (List.class.isAssignableFrom(attrType)) {
            return new ListEditor(getElementType(type, attrName));
        }

        if (Set.class.isAssignableFrom(attrType)) {
            return new SetEditor(getElementType(type, attrName));
        }

        if (Collection.class.isAssignableFrom(attrType)) {
            return new CollectionEditor(getElementType(type, attrName));
        }

        if (Map.class.isAssignableFrom(attrType)) {
            return new MapEditor(getMapKeyType(type, attrName), getMapValueType(type, attrName));
        }

        return PropertyEditorFactory.getInstance(attrType);
    }

    private class OgnlTypeConverter extends PropertyTypeConverter {
        @Override
        protected PropertyEditor getPropertyEditor(Class<?> type, String attrName, Class<?> attrType) {
            return ObjectMBean.this.getPropertyEditor(type, attrName, attrType);
        }
    }
}
