/**
 * 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.cxf.jaxrs.utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.Providers;

import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.ClassHelper;
import org.apache.cxf.common.util.PrimitiveUtils;
import org.apache.cxf.common.util.ProxyClassLoaderCache;
import org.apache.cxf.common.util.ReflectionUtil;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.jaxrs.ext.ContextProvider;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.ProtocolHeaders;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.impl.PathSegmentImpl;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalContextResolver;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalHttpHeaders;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalInvocationHandler;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalMessageContext;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalProtocolHeaders;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalProviders;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalProxy;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalRequest;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalSecurityContext;
import org.apache.cxf.jaxrs.impl.tl.ThreadLocalUriInfo;
import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
import org.apache.cxf.jaxrs.model.Parameter;
import org.apache.cxf.jaxrs.model.ParameterType;
import org.apache.cxf.jaxrs.provider.ProviderFactory;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;

public final class InjectionUtils {
    public static final Set<String> STANDARD_CONTEXT_CLASSES = new HashSet<>();
    public static final Set<String> VALUE_CONTEXTS = new HashSet<>();

    private static final boolean USE_JAXB;

    static {
        // JAX-RS 1.0-1.1
        STANDARD_CONTEXT_CLASSES.add(Application.class.getName());
        STANDARD_CONTEXT_CLASSES.add(UriInfo.class.getName());
        STANDARD_CONTEXT_CLASSES.add(HttpHeaders.class.getName());
        STANDARD_CONTEXT_CLASSES.add(Request.class.getName());
        STANDARD_CONTEXT_CLASSES.add(SecurityContext.class.getName());
        STANDARD_CONTEXT_CLASSES.add(Providers.class.getName());
        STANDARD_CONTEXT_CLASSES.add(ContextResolver.class.getName());
        STANDARD_CONTEXT_CLASSES.add("javax.servlet.http.HttpServletRequest");
        STANDARD_CONTEXT_CLASSES.add("javax.servlet.http.HttpServletResponse");
        STANDARD_CONTEXT_CLASSES.add("javax.servlet.ServletContext");
        // JAX-RS 2.0
        STANDARD_CONTEXT_CLASSES.add("javax.ws.rs.container.ResourceContext");
        STANDARD_CONTEXT_CLASSES.add("javax.ws.rs.container.ResourceInfo");
        STANDARD_CONTEXT_CLASSES.add("javax.ws.rs.core.Configuration");
        // JAX-RS 2.1
        STANDARD_CONTEXT_CLASSES.add("javax.ws.rs.sse.Sse");
        STANDARD_CONTEXT_CLASSES.add("javax.ws.rs.sse.SseEventSink");

        VALUE_CONTEXTS.add(Application.class.getName());
        VALUE_CONTEXTS.add("javax.ws.rs.sse.Sse");

        boolean useJaxb;
        try {
            ClassLoaderUtils.loadClass(
                    "javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter",
                    InjectionUtils.class);
            useJaxb = true;
        } catch (final ClassNotFoundException cnfe) {
            useJaxb = false;
        }
        USE_JAXB = useJaxb;
    }

    private static final Logger LOG = LogUtils.getL7dLogger(InjectionUtils.class);
    private static final ResourceBundle BUNDLE = BundleUtils.getBundle(InjectionUtils.class);

    private static final String SERVLET_CONFIG_CLASS_NAME = "javax.servlet.ServletConfig";
    private static final String SERVLET_CONTEXT_CLASS_NAME = "javax.servlet.ServletContext";
    private static final String HTTP_SERVLET_REQUEST_CLASS_NAME = "javax.servlet.http.HttpServletRequest";
    private static final String HTTP_SERVLET_RESPONSE_CLASS_NAME = "javax.servlet.http.HttpServletResponse";
    private static final String ENUM_CONVERSION_CASE_SENSITIVE = "enum.conversion.case.sensitive";

    private static final String IGNORE_MATRIX_PARAMETERS = "ignore.matrix.parameters";

    private static ProxyClassLoaderCache proxyClassLoaderCache =
        new ProxyClassLoaderCache();

    private InjectionUtils() {

    }

    public static Field getDeclaredField(Class<?> cls, String fieldName) {
        if (cls == null || cls == Object.class) {
            return null;
        }
        Field f = ReflectionUtil.getDeclaredField(cls, fieldName);
        if (f != null) {
            return f;
        }
        return getDeclaredField(cls.getSuperclass(), fieldName);
    }

    public static boolean isConcreteClass(Class<?> cls) {
        return !cls.isInterface() && !Modifier.isAbstract(cls.getModifiers());
    }

    private static ParameterizedType findGenericDeclaration(GenericDeclaration declaration, Type scope) {
        if (scope instanceof ParameterizedType) {
            ParameterizedType type = (ParameterizedType) scope;
            if (type.getRawType() == declaration) {
                return type;
            }
            scope = type.getRawType();
        }
        if (scope instanceof Class) {
            Class<?> classScope = (Class<?>)scope;
            ParameterizedType result = findGenericDeclaration(declaration, classScope.getGenericSuperclass());
            if (result == null) {
                for (Type type : classScope.getGenericInterfaces()) {
                    result = findGenericDeclaration(declaration, type);
                    if (result != null) {
                        break;
                    }
                }
            }
            return result;
        }
        return null;
    }

    public static Type getSuperType(Class<?> serviceClass, TypeVariable<?> var) {

        int pos = 0;
        GenericDeclaration genericDeclaration = var.getGenericDeclaration();
        TypeVariable<?>[] vars = genericDeclaration.getTypeParameters();
        for (; pos < vars.length; pos++) {
            if (vars[pos].getName().equals(var.getName())) {
                break;
            }
        }

        ParameterizedType genericSubtype = findGenericDeclaration(genericDeclaration, serviceClass);
        Type result = null;
        if (genericSubtype != null) {
            result = genericSubtype.getActualTypeArguments()[pos];
        }
        if (result instanceof TypeVariable) {
            result = getSuperType(serviceClass, (TypeVariable<?>) result);
        }

        if (result == null || result == Object.class) {
            for (Type bound : var.getBounds()) {
                if (bound != Object.class) {
                    result = bound;
                    break;
                }
            }
        }
        return result;
    }

    public static Method checkProxy(Method methodToInvoke, Object resourceObject) {
        if (Proxy.class.isInstance(resourceObject)) {
            String methodToInvokeName = methodToInvoke.getName();
            Class<?>[] methodToInvokeTypes = methodToInvoke.getParameterTypes();

            for (Class<?> c : resourceObject.getClass().getInterfaces()) {
                try {
                    return c.getMethod(methodToInvokeName, methodToInvokeTypes);
                } catch (NoSuchMethodException ex) {
                    //ignore
                }
                if (methodToInvokeTypes.length > 0) {
                    for (Method m : c.getMethods()) {
                        if (m.getName().equals(methodToInvokeName)
                            && m.getParameterTypes().length == methodToInvokeTypes.length) {
                            Class<?>[] methodTypes = m.getParameterTypes();
                            for (int i = 0; i < methodTypes.length; i++) {
                                if (!methodTypes[i].isAssignableFrom(methodToInvokeTypes[i])) {
                                    break;
                                }
                            }
                            return m;
                        }

                    }
                }

            }
        }
        return methodToInvoke;

    }

    public static void injectFieldValue(final Field f,
                                        final Object o,
                                        final Object v) {
        AccessController.doPrivileged(new PrivilegedAction<Object>() {
            public Object run() {
                try {
                    f.setAccessible(true);
                    f.set(o, v);
                } catch (IllegalAccessException ex) {
                    reportServerError("FIELD_ACCESS_FAILURE",
                                      f.getType().getName());
                }
                return null;
            }
        });
    }

    public static Object extractFieldValue(final Field f,
                                        final Object o) {
        return AccessController.doPrivileged(new PrivilegedAction<Object>() {
            public Object run() {
                try {
                    f.setAccessible(true);
                    return f.get(o);
                } catch (IllegalAccessException ex) {
                    reportServerError("FIELD_ACCESS_FAILURE",
                                      f.getType().getName());
                }
                return null;
            }
        });
    }

    public static Class<?> getActualType(Type genericType) {

        return getActualType(genericType, 0);
    }

    public static Class<?> getActualType(Type genericType, int pos) {

        if (genericType == null) {
            return null;
        }
        if (genericType == Object.class) {
            return (Class<?>)genericType;
        }
        if (!ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
            if (genericType instanceof TypeVariable) {
                genericType = getType(((TypeVariable<?>)genericType).getBounds(), pos);
            } else if (genericType instanceof WildcardType) {
                WildcardType wildcardType = (WildcardType)genericType;
                Type[] bounds = wildcardType.getLowerBounds();
                if (bounds.length == 0) {
                    bounds = wildcardType.getUpperBounds();
                }
                genericType = getType(bounds, pos);
            } else if (genericType instanceof GenericArrayType) {
                genericType = ((GenericArrayType)genericType).getGenericComponentType();
            }
            Class<?> cls = null;
            if (!(genericType instanceof ParameterizedType)) {
                cls = (Class<?>)genericType;
            } else {
                cls = (Class<?>)((ParameterizedType)genericType).getRawType();
            }
            return cls.isArray() ? cls.getComponentType() : cls;

        }
        ParameterizedType paramType = (ParameterizedType)genericType;
        Type t = getType(paramType.getActualTypeArguments(), pos);
        return t instanceof Class ? (Class<?>)t : getActualType(t, 0);
    }

    public static Type getType(Type[] types, int pos) {
        if (pos >= types.length) {
            throw new RuntimeException("No type can be found at position " + pos);
        }
        return types[pos];
    }

    public static Class<?> getRawType(Type genericType) {

        if (genericType instanceof Class) {
            return (Class<?>) genericType;
        } else if (genericType instanceof ParameterizedType) {
            ParameterizedType paramType = (ParameterizedType)genericType;
            Type t = paramType.getRawType();
            if (t instanceof Class) {
                return (Class<?>)t;
            }
        } else if (genericType instanceof GenericArrayType) {
            return getRawType(((GenericArrayType)genericType).getGenericComponentType());
        }
        return null;
    }


    public static Type[] getActualTypes(Type genericType) {
        if (genericType == null
            || !ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
            return null;
        }
        ParameterizedType paramType = (ParameterizedType)genericType;
        return paramType.getActualTypeArguments();
    }

    public static void injectThroughMethod(Object requestObject,
                                               Method method,
                                               Object parameterValue) {
        injectThroughMethod(requestObject, method, parameterValue, null);
    }

    public static void injectThroughMethod(Object requestObject,
                                               Method method,
                                               Object parameterValue,
                                               Message inMessage) {
        try {
            Method methodToInvoke = checkProxy(method, requestObject);
            methodToInvoke.invoke(requestObject, new Object[]{parameterValue});
        } catch (IllegalAccessException ex) {
            reportServerError("METHOD_ACCESS_FAILURE", method.getName());
        } catch (InvocationTargetException ex) {
            LOG.log(Level.SEVERE, ex.getCause().getMessage(), ex);
            Response r = JAXRSUtils.convertFaultToResponse(ex.getCause(), inMessage);
            if (r != null) {
                inMessage.getExchange().put(Response.class, r);
                throw new WebApplicationException();
            }
            reportServerError("METHOD_ACCESS_FAILURE", method.getName());
        } catch (Exception ex) {
            reportServerError("METHOD_INJECTION_FAILURE", method.getName());
        }
    }

    public static Object extractFromMethod(Object requestObject, Method method) {
        return extractFromMethod(requestObject, method, true);
    }

    public static Object extractFromMethod(Object requestObject,
                                           Method method,
                                           boolean logError) {
        try {
            Method methodToInvoke = checkProxy(method, requestObject);
            return methodToInvoke.invoke(requestObject);
        } catch (IllegalAccessException ex) {
            reportServerError("METHOD_ACCESS_FAILURE", method.getName(), logError);
        } catch (Exception ex) {
            reportServerError("METHOD_INJECTION_FAILURE", method.getName(), logError);
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    public static <T> T handleParameter(String value,
                                        boolean decoded,
                                        Class<T> pClass,
                                        Type genericType,
                                        Annotation[] paramAnns,
                                        ParameterType pType,
                                        Message message) {
        if (value == null) {
            return null;
        }
        if (pType == ParameterType.PATH) {
            if (PathSegment.class.isAssignableFrom(pClass)) {
                return pClass.cast(new PathSegmentImpl(value, decoded));
            } else if (!MessageUtils.getContextualBoolean(message, IGNORE_MATRIX_PARAMETERS)) {
                value = new PathSegmentImpl(value, false).getPath();
            }
        }

        value = decodeValue(value, decoded, pType);

        final Optional<ParamConverter<T>> converter = getParamConverter(pClass, genericType, paramAnns, message);
        Object result = null;
        try {
            if (converter.isPresent()) {
                result = converter.get().fromString(value);
            }
        } catch (IllegalArgumentException nfe) {
            throw createParamConversionException(pType, nfe);
        }
        if (result != null) {
            T theResult = null;
            if (pClass.isPrimitive()) {
                theResult = (T)result;
            } else {
                theResult = pClass.cast(result);
            }
            return theResult;
        } else if (converter.isPresent() && !pClass.isPrimitive()) {
            // The converter was applied and returned null value, acceptable
            // outcome for non-primitive type.
            return pClass.cast(result);
        }

        if (Number.class.isAssignableFrom(pClass) && "".equals(value)) {
            //pass empty string to boxed number type will result in 404
            return null;
        }
        if (Boolean.class == pClass) {
            // allow == checks for Boolean object
            pClass = (Class<T>) Boolean.TYPE;
        }
        if (pClass.isPrimitive()) {
            try {
                // cannot us pClass.cast as the pClass is something like
                // Boolean.TYPE (representing the boolean primitive) and
                // the object is a Boolean object
                return (T)PrimitiveUtils.read(value, pClass);
            } catch (NumberFormatException nfe) {
                throw createParamConversionException(pType, nfe);
            }
        }

        boolean adapterHasToBeUsed = false;
        Class<?> cls = pClass;
        Class<?> valueType = !USE_JAXB
                ? cls
                : JAXBUtils.getValueTypeFromAdapter(pClass, pClass, paramAnns);
        if (valueType != cls) {
            cls = valueType;
            adapterHasToBeUsed = true;
        }
        if (pClass == String.class && !adapterHasToBeUsed) {
            return pClass.cast(value);
        }
        // check constructors accepting a single String value
        try {
            Constructor<?> c = cls.getConstructor(new Class<?>[]{String.class});
            result = c.newInstance(new Object[]{value});
        } catch (NoSuchMethodException ex) {
            // try valueOf
        } catch (WebApplicationException ex) {
            throw ex;
        } catch (Exception ex) {
            Throwable t = getOrThrowActualException(ex);
            LOG.warning(new org.apache.cxf.common.i18n.Message("CLASS_CONSTRUCTOR_FAILURE",
                                                               BUNDLE,
                                                               pClass.getName()).toString());
            Response r = JAXRSUtils.toResponse(HttpUtils.getParameterFailureStatus(pType));
            throw ExceptionUtils.toHttpException(t, r);
        }
        if (result == null) {
            // check for valueOf(String) static methods
            String[] methodNames = cls.isEnum()
                ? new String[] {"fromString", "fromValue", "valueOf"}
                : new String[] {"valueOf", "fromString"};
            result = evaluateFactoryMethods(value, pType, result, cls, methodNames);
        }

        if (adapterHasToBeUsed) {
            // as the last resort, try XmlJavaTypeAdapters
            Object valueToReplace = result != null ? result : value;
            try {
                result = JAXBUtils.convertWithAdapter(valueToReplace, pClass, paramAnns);
            } catch (Throwable ex) {
                result = null;
            }
        }

        if (result == null) {
            reportServerError("WRONG_PARAMETER_TYPE", pClass.getName());
        }

        try {
            return pClass.cast(result);
        } catch (ClassCastException ex) {
            reportServerError("WRONG_PARAMETER_TYPE", pClass.getName());
            return null;
        }
    }

    private static RuntimeException createParamConversionException(ParameterType pType, Exception ex) {
        //
        //  For path, query & matrix parameters this is 404,
        //  for others 400...
        //
        if (pType == ParameterType.PATH || pType == ParameterType.QUERY
            || pType == ParameterType.MATRIX) {
            return ExceptionUtils.toNotFoundException(ex, null);
        }
        return ExceptionUtils.toBadRequestException(ex, null);
    }
    
    public static <T> Optional<ParamConverter<T>> getParamConverter(Class<T> pClass,
            Type genericType, Annotation[] anns, Message message) {
        
        if (message != null) {
            ServerProviderFactory pf = ServerProviderFactory.getInstance(message);
            ParamConverter<T> pm = pf.createParameterHandler(pClass, genericType, anns, message);
            return Optional.ofNullable(pm);
        }
        
        return Optional.empty();
    }

    public static <T> T createFromParameterHandler(String value,
                                                    Class<T> pClass,
                                                    Type genericType,
                                                    Annotation[] anns,
                                                    Message message) {
        return getParamConverter(pClass, genericType, anns, message)
            .map(pm -> pm.fromString(value))
            .orElse(null);
    }
    
    

    public static void reportServerError(String messageName, String parameter) {
        reportServerError(messageName, parameter, true);
    }

    public static void reportServerError(String messageName, String parameter, boolean logError) {
        org.apache.cxf.common.i18n.Message errorMessage =
            new org.apache.cxf.common.i18n.Message(messageName,
                                                   BUNDLE,
                                                   parameter);
        if (logError) {
            LOG.severe(errorMessage.toString());
        }
        Response r = JAXRSUtils.toResponseBuilder(Response.Status.INTERNAL_SERVER_ERROR)
                         .type(MediaType.TEXT_PLAIN_TYPE)
                         .entity(errorMessage.toString()).build();
        throw ExceptionUtils.toInternalServerErrorException(null, r);
    }

    private static Object evaluateFactoryMethods(String value, ParameterType pType, Object result,
                                                 Class<?> cls, String[] methodNames) {
        Exception factoryMethodEx = null;
        for (String mName : methodNames) {
            try {
                result = evaluateFactoryMethod(value, cls, mName);
                if (result != null) {
                    factoryMethodEx = null;
                    break;
                }
            } catch (Exception ex) {
                // If it is enum and the method name is "fromValue" then don't throw
                // the exception immediately but try the next factory method
                factoryMethodEx = ex;
                if (!cls.isEnum() || !"fromValue".equals(mName)) {
                    break;
                }
            }
        }
        if (factoryMethodEx != null) {
            Throwable t = getOrThrowActualException(factoryMethodEx);
            LOG.warning(new org.apache.cxf.common.i18n.Message("CLASS_VALUE_OF_FAILURE",
                                                               BUNDLE,
                                                               cls.getName()).toString());
            throw new WebApplicationException(t, HttpUtils.getParameterFailureStatus(pType));
        }
        return result;
    }

    private static <T> T evaluateFactoryMethod(String value,
                                               Class<T> pClass,
                                               String methodName)
        throws InvocationTargetException {
        try {
            Method m = pClass.getMethod(methodName, new Class<?>[]{String.class});
            if (Modifier.isStatic(m.getModifiers())) {
                return pClass.cast(m.invoke(null, new Object[]{value}));
            }
        } catch (NoSuchMethodException ex) {
            // no luck: try another factory methods
        } catch (IllegalAccessException ex) {
            // factory method is not accessible: try another
        }

        return null;
    }

    private static Throwable getOrThrowActualException(Throwable ex) {
        Throwable t = ex instanceof InvocationTargetException ? ((InvocationTargetException)ex).getCause() : ex;
        if (t instanceof WebApplicationException) {
            throw (WebApplicationException)t;
        }
        return t;
    }

    public static Object handleBean(Class<?> paramType, Annotation[] paramAnns,
                                    MultivaluedMap<String, String> values,
                                    ParameterType pType, Message message, boolean decoded) {
        Object bean = null;
        try {
            if (paramType.isInterface()) {
                paramType = org.apache.cxf.jaxrs.utils.JAXBUtils.getValueTypeFromAdapter(paramType,
                                                                                         paramType,
                                                                                         paramAnns);
            }
            bean = paramType.newInstance();
        } catch (IllegalAccessException ex) {
            reportServerError("CLASS_ACCESS_FAILURE", paramType.getName());
        } catch (Exception ex) {
            reportServerError("CLASS_INSTANTIATION_FAILURE", paramType.getName());
        }

        Map<String, MultivaluedMap<String, String>> parsedValues =
            new HashMap<>();
        for (Map.Entry<String, List<String>> entry : values.entrySet()) {
            String memberKey = entry.getKey();
            String beanKey = null;

            int idx = memberKey.indexOf('.');
            if (idx == -1) {
                beanKey = "." + memberKey;
            } else {
                beanKey = memberKey.substring(0, idx);
                memberKey = memberKey.substring(idx + 1);
            }

            MultivaluedMap<String, String> value = parsedValues.get(beanKey);
            if (value == null) {
                value = new MetadataMap<>();
                parsedValues.put(beanKey, value);
            }
            value.put(memberKey, entry.getValue());
        }

        if (!parsedValues.isEmpty()) {
            for (Map.Entry<String, MultivaluedMap<String, String>> entry : parsedValues.entrySet()) {
                String memberKey = entry.getKey();

                boolean isbean = !memberKey.startsWith(".");
                if (!isbean) {
                    memberKey = memberKey.substring(1);
                }

                Object setter = null;
                Object getter = null;
                for (Method m : paramType.getMethods()) {
                    if (m.getName().equalsIgnoreCase("set" + memberKey)
                        && m.getParameterTypes().length == 1) {
                        setter = m;
                    } else if (m.getName().equalsIgnoreCase("get" + memberKey)
                        || isBooleanType(m.getReturnType())
                           && m.getName().equalsIgnoreCase("is" + memberKey)) {
                        getter = m;
                    }
                    if (setter != null && getter != null) {
                        break;
                    }
                }
                if (setter == null) {
                    for (Field f : paramType.getFields()) {
                        if (f.getName().equalsIgnoreCase(memberKey)) {
                            setter = f;
                            getter = f;
                            break;
                        }
                    }
                }

                if (setter != null && getter != null) {
                    Class<?> type = null;
                    Type genericType = null;
                    Object paramValue = null;
                    if (setter instanceof Method) {
                        type = Method.class.cast(setter).getParameterTypes()[0];
                        genericType = Method.class.cast(setter).getGenericParameterTypes()[0];
                        paramValue = InjectionUtils.extractFromMethod(bean, (Method) getter);
                    } else {
                        type = Field.class.cast(setter).getType();
                        genericType = Field.class.cast(setter).getGenericType();
                        paramValue = InjectionUtils.extractFieldValue((Field) getter, bean);
                    }

                    List<MultivaluedMap<String, String>> processedValuesList =
                        processValues(type, genericType, entry.getValue(), isbean);

                    for (MultivaluedMap<String, String> processedValues : processedValuesList) {
                        if (InjectionUtils.isSupportedCollectionOrArray(type)) {
                            Object appendValue = InjectionUtils.injectIntoCollectionOrArray(type,
                                                            genericType, paramAnns, processedValues,
                                                            isbean, true,
                                                            pType, message);
                            paramValue = InjectionUtils.mergeCollectionsOrArrays(paramValue, appendValue,
                                                            genericType);
                        } else if (isSupportedMap(genericType)) {
                            Object appendValue = injectIntoMap(
                                genericType, paramAnns, processedValues, true, pType, message);
                            paramValue = mergeMap(paramValue, appendValue);

                        } else if (isbean) {
                            paramValue = InjectionUtils.handleBean(type, paramAnns, processedValues,
                                                            pType, message, decoded);
                        } else {
                            paramValue = InjectionUtils.handleParameter(
                                processedValues.values().iterator().next().get(0),
                                decoded, type, type, paramAnns, pType, message);
                        }

                        if (paramValue != null) {
                            if (setter instanceof Method) {
                                InjectionUtils.injectThroughMethod(bean, (Method) setter, paramValue);
                            } else {
                                InjectionUtils.injectFieldValue((Field) setter, bean, paramValue);
                            }
                        }
                    }
                }
            }
        }

        return bean;
    }

    @SuppressWarnings("unchecked")
    private static Object mergeMap(Object first, Object second) {
        if (first == null) {
            return second;
        } else if (first instanceof Map) {
            Map.class.cast(first).putAll((Map<?, ?>) second);
            return first;
        }
        return null;
    }

    // CHECKSTYLE:OFF
    private static Object injectIntoMap(Type genericType,
                                        Annotation[] paramAnns,
                                        MultivaluedMap<String, String> processedValues,
                                        boolean decoded,
                                        ParameterType pathParam, Message message) {
    // CHECKSTYLE:ON
        ParameterizedType paramType = (ParameterizedType) genericType;
        Class<?> keyType = (Class<?>)paramType.getActualTypeArguments()[0];
        Type secondType = InjectionUtils.getType(paramType.getActualTypeArguments(), 1);

        if (secondType instanceof ParameterizedType) {
            MultivaluedMap<Object, Object> theValues = new MetadataMap<>();
            ParameterizedType valueParamType = (ParameterizedType) secondType;
            Class<?> valueType = (Class<?>) InjectionUtils.getType(valueParamType
                               .getActualTypeArguments(), 0);

            for (Map.Entry<String, List<String>> processedValuesEntry : processedValues.entrySet()) {
                List<String> valuesList = processedValuesEntry.getValue();
                for (String value : valuesList) {
                    Object o = InjectionUtils.handleParameter(value,
                                       decoded, valueType, valueType, paramAnns, pathParam, message);
                    theValues.add(convertStringToPrimitive(processedValuesEntry.getKey(), keyType), o);
                }
            }
            return theValues;
        }
        Map<Object, Object> theValues = new HashMap<>();
        Class<?> valueType =
            (Class<?>) InjectionUtils.getType(paramType.getActualTypeArguments(), 1);
        for (Map.Entry<String, List<String>> processedValuesEntry : processedValues.entrySet()) {
            List<String> valuesList = processedValuesEntry.getValue();
            for (String value : valuesList) {
                Object o = InjectionUtils.handleParameter(value,
                                   decoded, valueType, valueType, paramAnns, pathParam, message);
                theValues.put(
                    convertStringToPrimitive(processedValuesEntry.getKey(), keyType),
                    o);
            }
        }
        return theValues;

    }


    private static boolean isSupportedMap(Type genericType) {
        Class<?> rawType = getRawType(genericType);
        if (Map.class.isAssignableFrom(rawType) && genericType instanceof ParameterizedType) {
            ParameterizedType paramType = (ParameterizedType) genericType;
            if (paramType.getActualTypeArguments().length == 2) {
                Class<?> firstType = getRawType(getType(paramType.getActualTypeArguments(), 0));
                Type secondType = getType(paramType.getActualTypeArguments(), 1);
                Class<?> secondRawType = getRawType(secondType);

                return InjectionUtils.isPrimitive(firstType)
                    && (InjectionUtils.isPrimitive(secondRawType)
                        || allowedMapListValue(secondRawType, secondType));
            }
        }
        return false;
    }

    private static boolean allowedMapListValue(Class<?> cls, Type type) {
        if (List.class.isAssignableFrom(cls)) {
            Class<?> listtype = getRawType(
                getType(((ParameterizedType)type).getActualTypeArguments(), 0));
            return InjectionUtils.isPrimitive(listtype);
        }
        return false;
    }

    private static List<MultivaluedMap<String, String>> processValues(Class<?> type, Type genericType,
                                        MultivaluedMap<String, String> values,
                                        boolean isbean) {
        List<MultivaluedMap<String, String>> valuesList =
            new ArrayList<>();

        if (isbean && InjectionUtils.isSupportedCollectionOrArray(type)) {
            Class<?> realType = InjectionUtils.getActualType(genericType);
            for (Map.Entry<String, List<String>> entry : values.entrySet()) {
                String memberKey = entry.getKey();
                Class<?> memberType = null;

                for (Method m : realType.getMethods()) {
                    if (m.getName().equalsIgnoreCase("set" + memberKey)
                        && m.getParameterTypes().length == 1) {
                        memberType = m.getParameterTypes()[0];
                        break;
                    }
                }
                if (memberType == null) {
                    for (Field f : realType.getFields()) {
                        if (f.getName().equalsIgnoreCase(memberKey)) {
                            memberType = f.getType();
                            break;
                        }
                    }
                }

                // Strip values tied to collection/array fields from beans that are within
                // collection/array themselves, the only way to support this would be to have
                // an indexing syntax for nested beans, perhaps like this:
                //    a(0).b=1&a(0).b=2&a(1).b=3&a(1).b=4
                // For now though we simply don't support this capability. To illustrate, the 'c'
                // param is dropped from this multivaluedmap example since it is a list:
                //    {c=[71, 81, 91, 72, 82, 92], a=[C1, C2], b=[790, 791]}
                if (memberType != null && InjectionUtils.isSupportedCollectionOrArray(memberType)) {
                    continue;
                }

                // Split multivaluedmap value list contents into separate multivaluedmap instances
                // whose list contents are only 1 level deep, for example:
                //    {a=[C1, C2], b=[790, 791]}
                // becomes these 2 separate multivaluedmap instances:
                //    {a=[C1], b=[790]} and {a=[C2], b=[791]}
                int idx = 0;
                for (String value : entry.getValue()) {
                    MultivaluedMap<String, String> splitValues =
                        (idx < valuesList.size()) ? valuesList.get(idx) : null;
                    if (splitValues == null) {
                        splitValues = new MetadataMap<>();
                        valuesList.add(splitValues);
                    }
                    splitValues.add(memberKey, value);
                    idx++;
                }
            }
        } else {
            valuesList.add(values);
        }

        return valuesList;
    }

    public static boolean isSupportedCollectionOrArray(Class<?> type) {
        return Collection.class.isAssignableFrom(type) || type.isArray();
    }

    @SuppressWarnings("unchecked")
    private static Object mergeCollectionsOrArrays(Object first, Object second, Type genericType) {
        if (first == null) {
            return second;
        } else if (first instanceof Collection) {
            Collection.class.cast(first).addAll((Collection<?>) second);
            return first;
        } else {
            int firstLen = Array.getLength(first);
            int secondLen = Array.getLength(second);
            Object mergedArray = Array.newInstance(InjectionUtils.getActualType(genericType),
                                                    firstLen + secondLen);
            System.arraycopy(first, 0, mergedArray, 0, firstLen);
            System.arraycopy(second, 0, mergedArray, firstLen, secondLen);
            return mergedArray;
        }
    }


    static Class<?> getCollectionType(Class<?> rawType) {
        Class<?> type = null;
        if (SortedSet.class.isAssignableFrom(rawType)) {
            type = TreeSet.class; //NOPMD
        } else if (Set.class.isAssignableFrom(rawType)) {
            type = HashSet.class; //NOPMD
        } else if (Collection.class.isAssignableFrom(rawType)) {
            type = ArrayList.class; //NOPMD
        }
        return type;

    }
    //CHECKSTYLE:OFF
    private static Object injectIntoCollectionOrArray(Class<?> rawType,
                                                      Type genericType,
                                                      Annotation[] paramAnns,
                                        MultivaluedMap<String, String> values,
                                        boolean isbean, boolean decoded,
                                        ParameterType pathParam, Message message) {
     //CHECKSTYLE:ON
        Class<?> type = getCollectionType(rawType);

        Class<?> realType = null;
        Type realGenericType = null;
        if (rawType.isArray()) {
            realType = rawType.getComponentType();
            realGenericType = realType;
        } else {
            Type[] types = getActualTypes(genericType);
            if (types == null || types.length == 0 || !(types[0] instanceof ParameterizedType)) {
                realType = getActualType(genericType);
                realGenericType = realType;
            } else {
                realType = getRawType(types[0]);
                realGenericType = types[0];
            }
        }
        Object theValues = null;
        if (type != null) {
            try {
                theValues = type.newInstance();
            } catch (IllegalAccessException ex) {
                reportServerError("CLASS_ACCESS_FAILURE", type.getName());
            } catch (Exception ex) {
                reportServerError("CLASS_INSTANTIATION_FAILURE", type.getName());
            }
        } else {
            theValues = Array.newInstance(realType, isbean ? 1 : values.values().iterator().next().size());
        }
        if (isbean) {
            Object o = InjectionUtils.handleBean(realType, paramAnns, values, pathParam, message, decoded);
            addToCollectionValues(theValues, o, 0);
        } else {
            List<String> valuesList = values.values().iterator().next();
            valuesList = checkPathSegment(valuesList, realType, pathParam);
            for (int ind = 0; ind < valuesList.size(); ind++) {
                Object o = InjectionUtils.handleParameter(valuesList.get(ind), decoded,
                               realType, realGenericType, paramAnns, pathParam, message);
                addToCollectionValues(theValues, o, ind);
            }
        }
        return theValues;
    }

    @SuppressWarnings("unchecked")
    private static void addToCollectionValues(Object theValues, Object o, int index) {
        if (o != null) {
            if (theValues instanceof Collection) {
                Collection.class.cast(theValues).add(o);
            } else if (theValues.getClass().getComponentType().isPrimitive()) {
                Array.set(theValues, index, o);
            } else {
                ((Object[]) theValues)[index] = o;
            }
        }
    }

    private static List<String> checkPathSegment(List<String> values, Class<?> type,
                                                 ParameterType pathParam) {
        if (pathParam != ParameterType.PATH || !PathSegment.class.isAssignableFrom(type)) {
            return values;
        }
        List<String> newValues = new ArrayList<>();
        for (String v : values) {
            String[] segments = v.split("/");
            for (String s : segments) {
                if (s.length() != 0) {
                    newValues.add(s);
                }
            }
            if (v.endsWith("/")) {
                newValues.add("");
            }
        }
        return newValues;
    }
    //
    //CHECKSTYLE:OFF
    public static Object createParameterObject(List<String> paramValues,
                                               Class<?> paramType,
                                               Type genericType,
                                               Annotation[] paramAnns,
                                               String defaultValue,
                                               boolean decoded,
                                               ParameterType pathParam,
                                               Message message) {
    //CHECKSTYLE:ON

        if (paramValues == null || paramValues.size() == 1 && paramValues.get(0) == null) {
            if (defaultValue != null) {
                paramValues = Collections.singletonList(defaultValue);
            } else {
                if (paramType.isPrimitive()) {
                    paramValues = Collections.singletonList(
                        boolean.class == paramType ? "false"
                            : char.class == paramType ? Character.toString('\u0000') : "0");
                } else if (InjectionUtils.isSupportedCollectionOrArray(paramType)) {
                    paramValues = Collections.emptyList();
                } else {
                    return null;
                }
            }
        }

        Object value = null;
        if (InjectionUtils.isSupportedCollectionOrArray(paramType)) {
            MultivaluedMap<String, String> paramValuesMap = new MetadataMap<>();
            paramValuesMap.put("", paramValues);
            value = InjectionUtils.injectIntoCollectionOrArray(paramType, genericType, paramAnns,
                                                paramValuesMap, false, decoded, pathParam, message);
        } else {
            String result = null;
            if (!paramValues.isEmpty()) {
                boolean isLast = pathParam == ParameterType.PATH ? true : false;
                result = isLast ? paramValues.get(paramValues.size() - 1)
                                : paramValues.get(0);
            }
            if (result != null) {
                value = InjectionUtils.handleParameter(result, decoded, paramType, genericType,
                                                       paramAnns, pathParam, message);
            }
        }
        return value;
    }

    // TODO : investigate the possibility of using generic proxies only
    @SuppressWarnings("unchecked")
    public static <T> ThreadLocalProxy<T> createThreadLocalProxy(Class<T> type) {
        ThreadLocalProxy<?> proxy = null;
        if (UriInfo.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalUriInfo();
        } else if (HttpHeaders.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalHttpHeaders();
        } else if (ProtocolHeaders.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalProtocolHeaders();
        } else if (SecurityContext.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalSecurityContext();
        } else if (ContextResolver.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalContextResolver<>();
        } else if (Request.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalRequest();
        }  else if (Providers.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalProviders();
        } else if (MessageContext.class.isAssignableFrom(type)) {
            proxy = new ThreadLocalMessageContext();
        }

        if (proxy == null && isServletApiContext(type.getName())) {
            proxy = createThreadLocalServletApiContext(type.getName());
        }
        if (proxy == null) {
            ClassLoader loader
                = proxyClassLoaderCache.getProxyClassLoader(Proxy.class.getClassLoader(),
                                                            new Class<?>[]{Proxy.class, ThreadLocalProxy.class, type});
            if (!canSeeAllClasses(loader, new Class<?>[]{Proxy.class, ThreadLocalProxy.class, type})) {
                LOG.log(Level.FINE, "find a loader from ProxyClassLoader cache,"
                    + " but can't see all interfaces");

                LOG.log(Level.FINE, "create a new one with parent  " + Proxy.class.getClassLoader());
                proxyClassLoaderCache.removeStaleProxyClassLoader(type);
                proxyClassLoaderCache.getProxyClassLoader(Proxy.class.getClassLoader(),
                                                          new Class<?>[]{Proxy.class, ThreadLocalProxy.class, type});


            }
            return (ThreadLocalProxy<T>)Proxy.newProxyInstance(loader,
                                   new Class[] {type, ThreadLocalProxy.class },
                                   new ThreadLocalInvocationHandler<T>());
        }

        return (ThreadLocalProxy<T>)proxy;
    }

    private static boolean canSeeAllClasses(ClassLoader loader, Class<?>[] interfaces) {
        for (Class<?> currentInterface : interfaces) {
            String ifName = currentInterface.getName();
            try {
                Class<?> ifClass = Class.forName(ifName, true, loader);
                if (ifClass != currentInterface) {
                    return false;
                }

            } catch (NoClassDefFoundError | ClassNotFoundException e) {
                return false;
            }
        }
        return true;
    }

    private static boolean isServletApiContext(String name) {
        return name.startsWith("javax.servlet.");
    }

    private static ThreadLocalProxy<?> createThreadLocalServletApiContext(String name) {
        String proxyClassName = null;
        if (HTTP_SERVLET_REQUEST_CLASS_NAME.equals(name)) {
            proxyClassName = "org.apache.cxf.jaxrs.impl.tl.ThreadLocalHttpServletRequest";
        } else if (HTTP_SERVLET_RESPONSE_CLASS_NAME.equals(name)) {
            proxyClassName = "org.apache.cxf.jaxrs.impl.tl.ThreadLocalHttpServletResponse";
        } else if (SERVLET_CONTEXT_CLASS_NAME.equals(name)) {
            proxyClassName = "org.apache.cxf.jaxrs.impl.tl.ThreadLocalServletContext";
        } else if (SERVLET_CONFIG_CLASS_NAME.equals(name)) {
            proxyClassName = "org.apache.cxf.jaxrs.impl.tl.ThreadLocalServletConfig";
        }
        if (proxyClassName != null) {
            try {
                return (ThreadLocalProxy<?>)ClassLoaderUtils.loadClass(proxyClassName, InjectionUtils.class)
                    .newInstance();
            } catch (Throwable t) {
                throw new RuntimeException(t);
            }
        }
        return null;
    }

    public static Method getGetterFromSetter(Method setter) throws Exception {
        return setter.getDeclaringClass().getMethod("get" + setter.getName().substring(3));
    }

    public static void injectContextProxiesAndApplication(AbstractResourceInfo cri,
                                                          Object instance,
                                                          Application app,
                                                          ProviderFactory factory) {
        if (!cri.contextsAvailable() || !cri.isSingleton()) {
            return;
        }
        synchronized (instance) {
            for (Map.Entry<Class<?>, Method> entry : cri.getContextMethods().entrySet()) {
                Method method = entry.getValue();
                Object value = null;
                Class<?> cls = method.getParameterTypes()[0];
                if (cls == Application.class) {
                    value = app;
                } else if (VALUE_CONTEXTS.contains(cls.getName()) && factory != null) {
                    ContextProvider<?> p = factory.createContextProvider(cls, null);
                    if (p != null) {
                        value = p.createContext(null);
                    }
                } else {
                    value = cri.getContextSetterProxy(method);
                }
                try {
                    if (value == InjectionUtils.extractFromMethod(instance,
                                                                  getGetterFromSetter(method),
                                                                  false)) {
                        continue;
                    }

                } catch (Throwable t) {
                    // continue
                }
                InjectionUtils.injectThroughMethod(instance, method, value);
            }

            for (Field f : cri.getContextFields()) {
                Object value = null;
                Class<?> cls = f.getType();
                if (cls == Application.class) {
                    value = app;
                } else if (VALUE_CONTEXTS.contains(cls.getName()) && factory != null) {
                    ContextProvider<?> p = factory.createContextProvider(cls, null);
                    if (p != null) {
                        value = p.createContext(null);
                    }
                } else {
                    value = cri.getContextFieldProxy(f);
                }
                try {
                    if (value == InjectionUtils.extractFieldValue(f, instance)) {
                        continue;
                    }
                } catch (Throwable t) {
                    // continue
                }
                InjectionUtils.injectFieldValue(f, instance, value);
            }
        }
    }

    public static void injectContextProxies(AbstractResourceInfo cri, Object instance) {
        injectContextProxiesAndApplication(cri, instance, null, null);
    }

    @SuppressWarnings("unchecked")
    public static void injectContextField(AbstractResourceInfo cri,
                                          Field f, Object o, Object value) {
        if (!cri.isSingleton()) {
            InjectionUtils.injectFieldValue(f, o, value);
        } else {
            ThreadLocalProxy<Object> proxy = (ThreadLocalProxy<Object>)cri.getContextFieldProxy(f);
            if (proxy != null) {
                proxy.set(value);
            }
        }
    }

    public static void injectContexts(Object requestObject,
                                 AbstractResourceInfo resource,
                                 Message message) {
        if (resource.contextsAvailable()) {
            injectContextMethods(requestObject, resource, message);
            injectContextFields(requestObject, resource, message);
            injectConstructorProxies(requestObject, resource, message);
        }
    }

    @SuppressWarnings("unchecked")
    public static void injectContextMethods(Object requestObject,
                                            AbstractResourceInfo cri,
                                            Message message) {

        for (Map.Entry<Class<?>, Method> entry : cri.getContextMethods().entrySet()) {
            Method method = entry.getValue();
            if (VALUE_CONTEXTS.contains(method.getParameterTypes()[0].getName()) && cri.isSingleton()) {
                continue;
            }
            Object o = JAXRSUtils.createContextValue(message,
                                              method.getGenericParameterTypes()[0],
                                              entry.getKey());

            if (o != null) {
                if (!cri.isSingleton()) {
                    InjectionUtils.injectThroughMethod(requestObject, method, o, message);
                } else {
                    ThreadLocalProxy<Object> proxy
                        = (ThreadLocalProxy<Object>)cri.getContextSetterProxy(method);
                    if (proxy != null) {
                        proxy.set(o);
                    }
                }

            }
        }
    }

    public static void injectContextFields(Object o,
                                           AbstractResourceInfo cri,
                                           Message m) {

        for (Field f : cri.getContextFields()) {
            if (VALUE_CONTEXTS.contains(f.getType().getName()) && cri.isSingleton()) {
                continue;
            }
            Object value = JAXRSUtils.createContextValue(m, f.getGenericType(), f.getType());
            InjectionUtils.injectContextField(cri, f, o, value);
        }
    }

    @SuppressWarnings("unchecked")
    public static void injectConstructorProxies(Object o,
                                                AbstractResourceInfo cri,
                                                Message m) {

        Map<Class<?>, ThreadLocalProxy<?>> proxies = cri.getConstructorProxies();
        if (proxies != null) {
            for (Map.Entry<Class<?>, ThreadLocalProxy<?>> entry : proxies.entrySet()) {
                Object value = JAXRSUtils.createContextValue(m, entry.getKey(), entry.getKey());
                ((ThreadLocalProxy<Object>)entry.getValue()).set(value);
            }
        }
    }

    public static MultivaluedMap<String, Object> extractValuesFromBean(Object bean, String baseName) {
        MultivaluedMap<String, Object> values = new MetadataMap<>();
        fillInValuesFromBean(bean, baseName, values);
        return values;
    }

    private static boolean isBooleanType(Class<?> cls) {
        return boolean.class == cls || Boolean.class == cls;
    }

    public static void fillInValuesFromBean(Object bean, String baseName,
                                            MultivaluedMap<String, Object> values) {
        for (Method m : bean.getClass().getMethods()) {
            String methodName = m.getName();
            boolean startsFromGet = methodName.startsWith("get");
            if ((startsFromGet
                || isBooleanType(m.getReturnType()) && methodName.startsWith("is"))
                && m.getParameterTypes().length == 0) {

                int minLen = startsFromGet ? 3 : 2;
                if (methodName.length() <= minLen) {
                    continue;
                }

                String propertyName = methodName.substring(minLen);
                if (propertyName.length() == 1) {
                    propertyName = propertyName.toLowerCase();
                } else {
                    propertyName = propertyName.substring(0, 1).toLowerCase()
                                   + propertyName.substring(1);
                }
                if (baseName.contains(propertyName)
                    || "class".equals(propertyName)
                    || "declaringClass".equals(propertyName)) {
                    continue;
                }
                if (!"".equals(baseName)) {
                    propertyName = baseName + "." + propertyName;
                }

                Object value = extractFromMethod(bean, m);
                if (value == null) {
                    continue;
                }
                if (isPrimitive(value.getClass()) || Date.class.isAssignableFrom(value.getClass())) {
                    values.putSingle(propertyName, value);
                } else if (value.getClass().isEnum()) {
                    values.putSingle(propertyName, value.toString());
                } else if (isSupportedCollectionOrArray(value.getClass())) {
                    List<Object> theValues = null;
                    if (value.getClass().isArray()) {
                        theValues = Arrays.asList((Object[])value);
                    } else if (value instanceof Set) {
                        theValues = new ArrayList<>((Set<?>)value);
                    } else {
                        theValues = CastUtils.cast((List<?>)value);
                    }
                    values.put(propertyName, theValues);
                } else if (Map.class.isAssignableFrom(value.getClass())) {
                    if (isSupportedMap(m.getGenericReturnType())) {
                        Map<Object, Object> map = CastUtils.cast((Map<?, ?>)value);
                        for (Map.Entry<Object, Object> entry : map.entrySet()) {
                            values.add(propertyName + "." + entry.getKey().toString(),
                                       entry.getValue().toString());
                        }
                    }
                } else {
                    fillInValuesFromBean(value, propertyName, values);
                }
            }
        }
    }

    public static Map<Parameter, Class<?>> getParametersFromBeanClass(Class<?> beanClass,
                                                                      ParameterType type,
                                                                      boolean checkIgnorable) {
        Map<Parameter, Class<?>> params = new LinkedHashMap<>();
        for (Method m : beanClass.getMethods()) {
            String methodName = m.getName();
            boolean startsFromGet = methodName.startsWith("get");
            if ((startsFromGet
                || isBooleanType(m.getReturnType()) && methodName.startsWith("is"))
                && m.getParameterTypes().length == 0) {

                int minLen = startsFromGet ? 3 : 2;
                if (methodName.length() <= minLen) {
                    continue;
                }
                String propertyName = StringUtils.uncapitalize(methodName.substring(minLen));
                if (m.getReturnType() == Class.class
                    || checkIgnorable && canPropertyBeIgnored(m, propertyName)) {
                    continue;
                }
                params.put(new Parameter(type, propertyName), m.getReturnType());
            }
        }
        return params;
    }

    private static boolean canPropertyBeIgnored(Method m, String propertyName) {
        for (Annotation ann : m.getAnnotations()) {
            String annType = ann.annotationType().getName();
            if ("org.apache.cxf.aegis.type.java5.IgnoreProperty".equals(annType)
                || "javax.xml.bind.annotation.XmlTransient".equals(annType)) {
                return true;
            }
        }
        return false;
    }
    public static boolean isPrimitive(Class<?> type) {
        return String.class == type
            || isPrimitiveOnly(type);
    }
    public static boolean isPrimitiveOnly(Class<?> type) {
        return type.isPrimitive()
            || Number.class.isAssignableFrom(type)
            || Boolean.class == type
            || Character.class == type;
    }

    public static String decodeValue(String value, boolean decode, ParameterType param) {
        if (!decode) {
            return value;
        }
        if (param == ParameterType.PATH || param == ParameterType.MATRIX) {
            return HttpUtils.pathDecode(value);
        }
        return HttpUtils.urlDecode(value);
    }

    public static void invokeLifeCycleMethod(Object instance, Method method) {
        if (method != null) {
            method = InjectionUtils.checkProxy(method, instance);
            try {
                method.invoke(instance, new Object[]{});
            } catch (InvocationTargetException ex) {
                String msg = "Method " + method.getName() + " can not be invoked"
                    + " due to InvocationTargetException";
                throw new WebApplicationException(JAXRSUtils.toResponseBuilder(500).entity(msg).build());
            } catch (IllegalAccessException ex) {
                String msg = "Method " + method.getName() + " can not be invoked"
                    + " due to IllegalAccessException";
                throw ExceptionUtils.toInternalServerErrorException(ex,
                    JAXRSUtils.toResponseBuilder(500).entity(msg).build());
            }
        }
    }
    public static Object convertStringToPrimitive(String value, Class<?> cls) {
        return convertStringToPrimitive(value, cls, new Annotation[]{});
    }
    public static Object convertStringToPrimitive(String value, Class<?> cls, Annotation[] anns) {
        Message m = JAXRSUtils.getCurrentMessage();
        Object obj = createFromParameterHandler(value, cls, cls, anns, m);
        if (obj != null) {
            return obj;
        }
        if (String.class == cls) {
            return value;
        } else if (cls.isPrimitive()) {
            return PrimitiveUtils.read(value, cls);
        } else if (cls.isEnum()) {
            if (m != null && !MessageUtils.getContextualBoolean(m, ENUM_CONVERSION_CASE_SENSITIVE, false)) {
                obj = invokeValueOf(value.toUpperCase(), cls);
            }
            if (obj == null) {
                try {
                    obj = invokeValueOf(value, cls);
                } catch (RuntimeException ex) {
                    if (m == null) {
                        obj = invokeValueOf(value.toUpperCase(), cls);
                    } else {
                        throw ex;
                    }
                }
            }
            return obj;
        } else {
            try {
                Constructor<?> c = cls.getConstructor(new Class<?>[]{String.class});
                return c.newInstance(new Object[]{value});
            } catch (Throwable ex) {
                // try valueOf
            }
            return invokeValueOf(value, cls);
        }
    }

    private static Object invokeValueOf(String value, Class<?> cls) {
        try {
            Method m = cls.getMethod("valueOf", new Class[]{String.class});
            return m.invoke(null, value);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public static Class<?> getRawResponseClass(Object targetObject) {
        if (targetObject != null) {
            Class<?> targetClass = targetObject.getClass();
            return ClassHelper.getRealClassFromClass(targetClass);
        }
        return null;
    }

    public static Type getGenericResponseType(Method invoked,
                                        Class<?> serviceCls,
                                        Object targetObject,
                                        Class<?> targetType,
                                        Exchange exchange) {
        if (targetObject == null) {
            return null;
        }
        Type type = null;
        if (GenericEntity.class.isAssignableFrom(targetObject.getClass())) {
            type = processGenericTypeIfNeeded(serviceCls, targetType, ((GenericEntity<?>)targetObject).getType());
        } else if (invoked == null
                   || !invoked.getReturnType().isAssignableFrom(targetType)) {
            // when a method has been invoked it is still possible that either an ExceptionMapper
            // or a ResponseHandler filter overrides a response entity; if it happens then
            // the Type is the class of the response object, unless this new entity is assignable
            // to invoked.getReturnType(); same applies to the case when a method returns Response
            type = targetObject.getClass();
        } else {
            type = processGenericTypeIfNeeded(serviceCls, targetType,  invoked.getGenericReturnType());
        }

        return type;
    }
    public static Class<?> updateParamClassToTypeIfNeeded(Class<?> paramCls, Type type) {
        if (paramCls != type && type instanceof Class) {
            Class<?> clsType = (Class<?>)type;
            if (paramCls.isAssignableFrom(clsType)
                || clsType != Object.class && !clsType.isInterface() && clsType.isAssignableFrom(paramCls)) {
                paramCls = clsType;
            }
        }
        return paramCls;
    }

    public static Type processGenericTypeIfNeeded(Class<?> serviceCls, Class<?> paramCls, Type type) {

        if (type instanceof TypeVariable) {
            type = InjectionUtils.getSuperType(serviceCls, (TypeVariable<?>)type);
        } else if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType)type;
            if (pt.getActualTypeArguments()[0] instanceof TypeVariable
                && isSupportedCollectionOrArray(getRawType(pt))) {
                TypeVariable<?> typeVar = (TypeVariable<?>)pt.getActualTypeArguments()[0];
                Type theType = InjectionUtils.getSuperType(serviceCls, typeVar);
                if (theType instanceof Class) {
                    type = new ParameterizedCollectionType((Class<?>)theType);
                } else {
                    type = processGenericTypeIfNeeded(serviceCls, paramCls, theType);
                    type = new ParameterizedCollectionType(type);
                }
            }
        }

        if (type == null || type == Object.class) {
            type = paramCls;
        }
        return type;

    }

    public static Object getEntity(Object o) {
        return o instanceof GenericEntity ? ((GenericEntity<?>)o).getEntity() : o;
    }
}
