package org.qfox.jestful.server.converter;

import org.qfox.jestful.core.converter.ValueConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.PropertyDescriptor;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class BeanConverter implements Converter {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public boolean supports(Class<?> clazz) {
        return !(clazz.isInterface() || clazz.isAnnotation() || clazz.isEnum() || clazz.isArray() || Modifier.isAbstract(clazz.getModifiers()));
    }

    public <T> T convert(String name, Class<T> clazz, boolean decoded, String charset, Map<String, String[]> map, ConversionProvider provider) throws ConversionException, UnsupportedEncodingException {
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(ValueConverter.class)) continue;
            if (!Modifier.isStatic(method.getModifiers())) continue;
            if (method.getReturnType() != clazz) continue;
            if (method.getGenericParameterTypes().length != 1) continue;
            Type type = method.getGenericParameterTypes()[0];
            Object value = provider.convert(name, type, decoded, charset, map);
            try {
                return clazz.cast(method.invoke(null, value));
            } catch (Exception e) {
                throw new IncompatibleConversionException(e, name, clazz, map, provider);
            }
        }

        Constructor<?>[] constructors = clazz.getConstructors();
        for (Constructor<?> constructor : constructors) {
            if (!constructor.isAnnotationPresent(ValueConverter.class)) continue;
            if (constructor.getParameterTypes().length != 1) continue;
            Type type = constructor.getGenericParameterTypes()[0];
            Object value = provider.convert(name, type, decoded, charset, map);
            try {
                return clazz.cast(constructor.newInstance(value));
            } catch (Exception e) {
                throw new IncompatibleConversionException(e, name, clazz, map, provider);
            }
        }

        T bean;
        try {
            bean = clazz.newInstance();
        } catch (Exception e) {
            throw new UnsupportedConversionException(e, name, clazz, map, provider);
        }
        for (String key : map.keySet()) {
            if (key.startsWith(name + ".") && key.length() > name.length() + 1) {
                int index = key.indexOf('.');
                String property = key.substring(index + 1);
                String[] values = map.get(key) != null ? map.get(key).clone() : new String[0];
                Map<String, String[]> _map = new LinkedHashMap<String, String[]>();
                _map.put(property, values);
                try {
                    PropertyDescriptor descriptor = new PropertyDescriptor(property.split("\\.")[0], clazz);
                    Object value = provider.convert(property.split("\\.")[0], descriptor.getPropertyType(), decoded, charset, _map);
                    descriptor.getWriteMethod().invoke(bean, value);
                } catch (ConversionException e) {
                    throw e;
                } catch (Exception e) {
                    logger.warn("fail to convert property [" + property + "] of class [" + clazz + "] with values " + Arrays.toString(values), e);
                }
            }
        }
        return bean;
    }

    public boolean supports(ParameterizedType type) {
        return type.getRawType() instanceof Class<?>;
    }

    public Object convert(String name, ParameterizedType type, boolean decoded, String charset, Map<String, String[]> map, ConversionProvider provider) throws ConversionException, UnsupportedEncodingException {
        Class<?> clazz = (Class<?>) type.getRawType();
        Object bean;
        try {
            bean = clazz.newInstance();
        } catch (Exception e) {
            throw new UnsupportedConversionException(e, name, type, map, provider);
        }
        List<?> variables = Arrays.asList(clazz.getTypeParameters());
        for (String key : map.keySet()) {
            if (key.startsWith(name + ".") && key.length() > name.length() + 1) {
                int index = key.indexOf('.');
                String property = key.substring(index + 1);
                String[] values = map.get(key) != null ? map.get(key).clone() : new String[0];
                Map<String, String[]> _map = new LinkedHashMap<String, String[]>();
                _map.put(property, values);
                try {
                    PropertyDescriptor descriptor = new PropertyDescriptor(property.split("\\.")[0], clazz);
                    Type _type = descriptor.getReadMethod().getGenericReturnType();
                    if (_type instanceof TypeVariable<?>) {
                        int _index = variables.indexOf(_type);
                        _type = type.getActualTypeArguments()[_index];
                    }
                    Object value = provider.convert(property.split("\\.")[0], _type, decoded, charset, _map);
                    descriptor.getWriteMethod().invoke(bean, value);
                } catch (ConversionException e) {
                    throw e;
                } catch (Exception e) {
                    throw new UnknownConversionException(e, name, clazz, map, provider);
                }
            }
        }
        return bean;
    }

}

@@ -3981,32 +3981,1467 @@
 e.getRawType();%0A
+%0A        Method%5B%5D methods = clazz.getMethods();%0A        for (Method method : methods) %7B%0A            if (!method.isAnnotationPresent(ValueConverter.class)) continue;%0A            if (!Modifier.isStatic(method.getModifiers())) continue;%0A            if (method.getReturnType() != clazz) continue;%0A            if (method.getGenericParameterTypes().length != 1) continue;%0A            Type parameterType = method.getGenericParameterTypes()%5B0%5D;%0A            Object value = provider.convert(name, parameterType, decoded, charset, map);%0A            try %7B%0A                return clazz.cast(method.invoke(null, value));%0A            %7D catch (Exception e) %7B%0A                throw new IncompatibleConversionException(e, name, clazz, map, provider);%0A            %7D%0A        %7D%0A%0A        Constructor%3C?%3E%5B%5D constructors = clazz.getConstructors();%0A        for (Constructor%3C?%3E constructor : constructors) %7B%0A            if (!constructor.isAnnotationPresent(ValueConverter.class)) continue;%0A            if (constructor.getParameterTypes().length != 1) continue;%0A            Type parameterType = constructor.getGenericParameterTypes()%5B0%5D;%0A            Object value = provider.convert(name, parameterType, decoded, charset, map);%0A            try %7B%0A                return clazz.cast(constructor.newInstance(value));%0A            %7D catch (Exception e) %7B%0A                throw new IncompatibleConversionException(e, name, clazz, map, provider);%0A            %7D%0A        %7D%0A%0A
         Object b

Add Extendable Value Conversion