/**
 * 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.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.Consumes;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Cookie;
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.Response.ResponseBuilder;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import javax.xml.namespace.QName;

import org.apache.cxf.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PackageUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.common.util.ReflectionUtil;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.jaxrs.JAXRSServiceImpl;
import org.apache.cxf.jaxrs.ext.ContextProvider;
import org.apache.cxf.jaxrs.ext.DefaultMethod;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.MessageContextImpl;
import org.apache.cxf.jaxrs.ext.ProtocolHeaders;
import org.apache.cxf.jaxrs.ext.ProtocolHeadersImpl;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.impl.AsyncResponseImpl;
import org.apache.cxf.jaxrs.impl.ContainerRequestContextImpl;
import org.apache.cxf.jaxrs.impl.ContainerResponseContextImpl;
import org.apache.cxf.jaxrs.impl.HttpHeadersImpl;
import org.apache.cxf.jaxrs.impl.MediaTypeHeaderProvider;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.impl.PathSegmentImpl;
import org.apache.cxf.jaxrs.impl.ProvidersImpl;
import org.apache.cxf.jaxrs.impl.ReaderInterceptorContextImpl;
import org.apache.cxf.jaxrs.impl.ReaderInterceptorMBR;
import org.apache.cxf.jaxrs.impl.RequestImpl;
import org.apache.cxf.jaxrs.impl.ResourceContextImpl;
import org.apache.cxf.jaxrs.impl.ResourceInfoImpl;
import org.apache.cxf.jaxrs.impl.ResponseBuilderImpl;
import org.apache.cxf.jaxrs.impl.ResponseImpl;
import org.apache.cxf.jaxrs.impl.SecurityContextImpl;
import org.apache.cxf.jaxrs.impl.UriInfoImpl;
import org.apache.cxf.jaxrs.impl.WriterInterceptorContextImpl;
import org.apache.cxf.jaxrs.impl.WriterInterceptorMBW;
import org.apache.cxf.jaxrs.model.BeanParamInfo;
import org.apache.cxf.jaxrs.model.BeanResourceInfo;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.model.ClassResourceInfoComparator;
import org.apache.cxf.jaxrs.model.MethodInvocationInfo;
import org.apache.cxf.jaxrs.model.OperationResourceInfo;
import org.apache.cxf.jaxrs.model.OperationResourceInfoComparator;
import org.apache.cxf.jaxrs.model.OperationResourceInfoStack;
import org.apache.cxf.jaxrs.model.Parameter;
import org.apache.cxf.jaxrs.model.ParameterType;
import org.apache.cxf.jaxrs.model.ProviderInfo;
import org.apache.cxf.jaxrs.model.URITemplate;
import org.apache.cxf.jaxrs.provider.AbstractConfigurableProvider;
import org.apache.cxf.jaxrs.provider.ProviderFactory;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.phase.PhaseInterceptorChain;
import org.apache.cxf.service.Service;

public final class JAXRSUtils {

    public static final MediaType ALL_TYPES = new MediaType();
    public static final String ROOT_RESOURCE_CLASS = "root.resource.class";
    public static final String IGNORE_MESSAGE_WRITERS = "ignore.message.writers";
    public static final String ROOT_INSTANCE = "service.root.instance";
    public static final String ROOT_PROVIDER = "service.root.provider";
    public static final String EXCEPTION_FROM_MAPPER = "exception.from.mapper";
    public static final String SECOND_JAXRS_EXCEPTION = "second.jaxrs.exception";
    public static final String PARTIAL_HIERARCHICAL_MEDIA_SUBTYPE_CHECK = 
        "media.subtype.partial.check"; 
    public static final String DOC_LOCATION = "wadl.location";
    public static final String MEDIA_TYPE_Q_PARAM = "q";
    public static final String MEDIA_TYPE_QS_PARAM = "qs";
    private static final String MEDIA_TYPE_DISTANCE_PARAM = "d";
    private static final String DEFAULT_CONTENT_TYPE = "default.content.type";
    private static final String KEEP_SUBRESOURCE_CANDIDATES = "keep.subresource.candidates";
    private static final Logger LOG = LogUtils.getL7dLogger(JAXRSUtils.class);
    private static final ResourceBundle BUNDLE = BundleUtils.getBundle(JAXRSUtils.class);
    private static final String PATH_SEGMENT_SEP = "/";
    private static final String REPORT_FAULT_MESSAGE_PROPERTY = "org.apache.cxf.jaxrs.report-fault-message";
    private static final String NO_CONTENT_EXCEPTION = "javax.ws.rs.core.NoContentException";
    private static final String HTTP_CHARSET_PARAM = "charset";
    private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; 
    private static final Set<Class<?>> STREAMING_OUT_TYPES = new HashSet<Class<?>>(
        Arrays.asList(InputStream.class, Reader.class, StreamingOutput.class));
    
    private JAXRSUtils() {        
    }
    
    public static boolean isStreamingOutType(Class<?> type) {
        return STREAMING_OUT_TYPES.contains(type);
    }
    
    public static List<PathSegment> getPathSegments(String thePath, boolean decode) {
        return getPathSegments(thePath, decode, true);
    }
    
    public static List<PathSegment> getPathSegments(String thePath, boolean decode, 
                                                    boolean ignoreLastSlash) {
        String[] segments = StringUtils.split(thePath, "/");
        List<PathSegment> theList = new ArrayList<PathSegment>();
        for (String path : segments) {
            if (!StringUtils.isEmpty(path)) {
                theList.add(new PathSegmentImpl(path, decode));
            }
        }
        int len = thePath.length();
        if (len > 0 && thePath.charAt(len - 1) == '/') {
            String value = ignoreLastSlash ? "" : "/";
            theList.add(new PathSegmentImpl(value, false));
        }
        return theList;
    }

    private static String[] getUserMediaTypes(Object provider, boolean consumes) {
        String[] values = null;
        if (AbstractConfigurableProvider.class.isAssignableFrom(provider.getClass())) {
            List<String> types = null;
            if (consumes) {
                types = ((AbstractConfigurableProvider)provider).getConsumeMediaTypes();
            } else {
                types = ((AbstractConfigurableProvider)provider).getProduceMediaTypes();
            }
            if (types != null) {
                values = types.size() > 0 ? types.toArray(new String[types.size()])
                                           : new String[]{"*/*"};
            }
        }
        return values;
    }
    
    public static List<MediaType> getProviderConsumeTypes(MessageBodyReader<?> provider) {
        String[] values = getUserMediaTypes(provider, true);
        
        if (values == null) {
            return getConsumeTypes(provider.getClass().getAnnotation(Consumes.class));
        } else {
            return JAXRSUtils.getMediaTypes(values);
        }
    }
    
    public static List<MediaType> getProviderProduceTypes(MessageBodyWriter<?> provider) {
        String[] values = getUserMediaTypes(provider, false);
        if (values == null) {
            return getProduceTypes(provider.getClass().getAnnotation(Produces.class));
        } else {
            return JAXRSUtils.getMediaTypes(values);
        }
    }
    
    public static List<MediaType> getMediaTypes(String[] values) {
        List<MediaType> supportedMimeTypes = new ArrayList<MediaType>(values.length);
        for (int i = 0; i < values.length; i++) {
            supportedMimeTypes.addAll(parseMediaTypes(values[i]));    
        }
        return supportedMimeTypes;
    }
    
    public static void injectParameters(OperationResourceInfo ori,
                                        Object requestObject,
                                        Message message) {
        injectParameters(ori, ori.getClassResourceInfo(), requestObject, message);    
    }
    
    @SuppressWarnings("unchecked")
    public static void injectParameters(OperationResourceInfo ori,
                                        BeanResourceInfo bri,
                                        Object requestObject,
                                        Message message) {
                
        if (bri.isSingleton() 
            && (!bri.getParameterMethods().isEmpty() || !bri.getParameterFields().isEmpty())) {
            LOG.fine("Injecting request parameters into singleton resource is not thread-safe");
        }
        // Param methods
        MultivaluedMap<String, String> values = 
            (MultivaluedMap<String, String>)message.get(URITemplate.TEMPLATE_PARAMETERS);
        for (Method m : bri.getParameterMethods()) {
            Parameter p = ResourceUtils.getParameter(0, m.getAnnotations(), 
                                                     m.getParameterTypes()[0]);
            Object o;
            
            if (p.getType() == ParameterType.BEAN) {
                o = createBeanParamValue(message, m.getParameterTypes()[0], ori);    
            } else {
                o = createHttpParameterValue(p, 
                                                m.getParameterTypes()[0],
                                                m.getGenericParameterTypes()[0],
                                                m.getParameterAnnotations()[0],
                                                message,
                                                values,
                                                ori);
            }
            InjectionUtils.injectThroughMethod(requestObject, m, o, message);
        }
        // Param fields
        for (Field f : bri.getParameterFields()) {
            Parameter p = ResourceUtils.getParameter(0, f.getAnnotations(), 
                                                     f.getType());
            Object o = null;
            
            if (p.getType() == ParameterType.BEAN) {
                o = createBeanParamValue(message, f.getType(), ori);    
            } else {
                o = createHttpParameterValue(p, 
                                                f.getType(),
                                                f.getGenericType(),
                                                f.getAnnotations(),
                                                message,
                                                values,
                                                ori);
            }
            InjectionUtils.injectFieldValue(f, requestObject, o);
        }
    }
    
    public static Map<ClassResourceInfo, MultivaluedMap<String, String>> selectResourceClass(
        List<ClassResourceInfo> resources, String path, Message message) {
        
        boolean isFineLevelLoggable = LOG.isLoggable(Level.FINE); 
        if (isFineLevelLoggable) {
            LOG.fine(new org.apache.cxf.common.i18n.Message("START_CRI_MATCH", 
                                                        BUNDLE, 
                                                        path).toString());
        }
        if (resources.size() == 1) {
            MultivaluedMap<String, String> values = new MetadataMap<String, String>();
            return resources.get(0).getURITemplate().match(path, values)
                   ? Collections.singletonMap(resources.get(0), values) : null;
        }
        
        SortedMap<ClassResourceInfo, MultivaluedMap<String, String>> candidateList = 
            new TreeMap<ClassResourceInfo, MultivaluedMap<String, String>>(
                new ClassResourceInfoComparator(message));
        
        for (ClassResourceInfo cri : resources) {
            MultivaluedMap<String, String> map = new MetadataMap<String, String>();
            if (cri.getURITemplate().match(path, map)) {
                candidateList.put(cri, map);
                if (isFineLevelLoggable) {
                    LOG.fine(new org.apache.cxf.common.i18n.Message("CRI_SELECTED_POSSIBLY", 
                                                                BUNDLE, 
                                                                cri.getServiceClass().getName(),
                                                                path, 
                                                                cri.getURITemplate().getValue()).toString());
                }
            } else if (isFineLevelLoggable) {
                LOG.fine(new org.apache.cxf.common.i18n.Message("CRI_NO_MATCH", 
                                                                BUNDLE, 
                                                                path,
                                                                cri.getServiceClass().getName()).toString());
            }
        }
        
        if (!candidateList.isEmpty()) {
            Map<ClassResourceInfo, MultivaluedMap<String, String>> cris = 
                new LinkedHashMap<ClassResourceInfo, MultivaluedMap<String, String>>(candidateList.size());
            ClassResourceInfo firstCri = null;
            for (Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> entry : candidateList.entrySet()) {
                ClassResourceInfo cri = entry.getKey();
                if (cris.isEmpty()) {
                    firstCri = cri;
                    cris.put(cri, entry.getValue());
                } else if (URITemplate.compareTemplates(firstCri.getURITemplate(), cri.getURITemplate()) == 0) {
                    cris.put(cri, entry.getValue());
                } else {
                    break;
                }
                if (isFineLevelLoggable) {
                    LOG.fine(new org.apache.cxf.common.i18n.Message("CRI_SELECTED", 
                                                             BUNDLE, 
                                                             cri.getServiceClass().getName(),
                                                             path, cri.getURITemplate().getValue()).toString());
                }
            }
            return cris;
        }
        
        return null;
    }
    public static OperationResourceInfo findTargetMethod(
        Map<ClassResourceInfo, MultivaluedMap<String, String>> matchedResources,
        Message message,
        String httpMethod, 
        MultivaluedMap<String, String> matchedValues,
        String requestContentType, 
        List<MediaType> acceptContentTypes) {
        return findTargetMethod(matchedResources, message, httpMethod, matchedValues,
                                requestContentType, acceptContentTypes, true, true);
    }
    //CHECKSTYLE:OFF
    public static OperationResourceInfo findTargetMethod(
        Map<ClassResourceInfo, MultivaluedMap<String, String>> matchedResources,
        Message message,
        String httpMethod, 
        MultivaluedMap<String, String> matchedValues,
        String requestContentType, 
        List<MediaType> acceptContentTypes,
        boolean throwException,
        boolean recordMatchedUri) {
    //CHECKSTYLE:ON
        final boolean isFineLevelLoggable = LOG.isLoggable(Level.FINE);
        final boolean getMethod = HttpMethod.GET.equals(httpMethod);
        
        MediaType requestType;
        try {
            requestType = toMediaType(requestContentType);
        } catch (IllegalArgumentException ex) {
            throw ExceptionUtils.toNotSupportedException(ex, null);
        }
        
        SortedMap<OperationResourceInfo, MultivaluedMap<String, String>> candidateList = 
            new TreeMap<OperationResourceInfo, MultivaluedMap<String, String>>(
                new OperationResourceInfoComparator(message, httpMethod, 
                                                    getMethod, requestType, acceptContentTypes));

        int pathMatched = 0;
        int methodMatched = 0;
        int consumeMatched = 0;
        
        List<OperationResourceInfo> finalPathSubresources = null;
        for (Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> rEntry : matchedResources.entrySet()) {
            ClassResourceInfo resource = rEntry.getKey();
            MultivaluedMap<String, String> values = rEntry.getValue();
            
            String path = getCurrentPath(values);
            if (isFineLevelLoggable) {
                org.apache.cxf.common.i18n.Message msg = 
                    new org.apache.cxf.common.i18n.Message("START_OPER_MATCH", 
                                                           BUNDLE,
                                                           resource.getServiceClass().getName());
                LOG.fine(msg.toString());
                
            }
            
            for (OperationResourceInfo ori : resource.getMethodDispatcher().getOperationResourceInfos()) {
                boolean added = false;
                
                URITemplate uriTemplate = ori.getURITemplate();
                MultivaluedMap<String, String> map = new MetadataMap<String, String>(values);
                if (uriTemplate != null && uriTemplate.match(path, map)) {
                    String finalGroup = map.getFirst(URITemplate.FINAL_MATCH_GROUP);
                    boolean finalPath = StringUtils.isEmpty(finalGroup) || PATH_SEGMENT_SEP.equals(finalGroup);
                    
                    if (ori.isSubResourceLocator()) {
                        candidateList.put(ori, map);
                        if (finalPath) {
                            if (finalPathSubresources == null) {
                                finalPathSubresources = new LinkedList<OperationResourceInfo>();
                            }
                            finalPathSubresources.add(ori);
                        }
                        added = true;
                    } else if (finalPath) {
                        pathMatched++;
                        if (matchHttpMethod(ori.getHttpMethod(), httpMethod)) {
                            methodMatched++;
                            //CHECKSTYLE:OFF
                            if (getMethod || matchConsumeTypes(requestType, ori)) {
                                consumeMatched++;
                                for (MediaType acceptType : acceptContentTypes) {
                                    if (matchProduceTypes(acceptType, ori)) {
                                        candidateList.put(ori, map);
                                        added = true;
                                        break;
                                    }
                                }
                            }
                            //CHECKSTYLE:ON
                        }
                    }
                } 
                if (isFineLevelLoggable) {
                    if (added) {
                        LOG.fine(new org.apache.cxf.common.i18n.Message("OPER_SELECTED_POSSIBLY", 
                                  BUNDLE, 
                                  ori.getMethodToInvoke().getName()).toString());
                    } else {
                        logNoMatchMessage(ori, path, httpMethod, requestType, acceptContentTypes);
                    }
                }
            }
        }
        if (finalPathSubresources != null && pathMatched > 0
            && !MessageUtils.getContextualBoolean(message, KEEP_SUBRESOURCE_CANDIDATES, false)) {
            for (OperationResourceInfo key : finalPathSubresources) {
                candidateList.remove(key);
            }
        }        
        if (!candidateList.isEmpty()) {
            Map.Entry<OperationResourceInfo, MultivaluedMap<String, String>> firstEntry = 
                candidateList.entrySet().iterator().next();
            matchedValues.clear();
            matchedValues.putAll(firstEntry.getValue());
            OperationResourceInfo ori = firstEntry.getKey();
            if (headMethodPossible(ori.getHttpMethod(), httpMethod)) {
                LOG.info(new org.apache.cxf.common.i18n.Message("GET_INSTEAD_OF_HEAD", 
                         BUNDLE, ori.getClassResourceInfo().getServiceClass().getName(), 
                         ori.getMethodToInvoke().getName()).toString());
            }
            if (isFineLevelLoggable) {
                LOG.fine(new org.apache.cxf.common.i18n.Message("OPER_SELECTED", 
                               BUNDLE, ori.getMethodToInvoke().getName(), 
                               ori.getClassResourceInfo().getServiceClass().getName()).toString());
            }
            if (!ori.isSubResourceLocator()) {
                MediaType responseMediaType = intersectSortMediaTypes(acceptContentTypes,
                                                                      ori.getProduceTypes(),
                                                                      false).get(0);
                message.getExchange().put(Message.CONTENT_TYPE, mediaTypeToString(responseMediaType, 
                                                                                  MEDIA_TYPE_Q_PARAM, 
                                                                                  MEDIA_TYPE_QS_PARAM));
            }
            if (recordMatchedUri) {
                pushOntoStack(ori, matchedValues, message);
            }
            return ori;
        }
        
        if (!throwException) {
            return null;
        }
        
        int status;
        
        // criteria matched the least number of times will determine the error code;
        // priority : path, method, consumes, produces;
        if (pathMatched == 0) {
            status = 404;
        } else if (methodMatched == 0) {
            status = 405;
        } else if (consumeMatched == 0) {
            status = 415;
        } else {
            // Not a single Produces match
            status = 406;
        }
        Map.Entry<ClassResourceInfo, MultivaluedMap<String, String>> firstCri = 
            matchedResources.entrySet().iterator().next();
        String name = firstCri.getKey().isRoot() ? "NO_OP_EXC" : "NO_SUBRESOURCE_METHOD_FOUND";
        org.apache.cxf.common.i18n.Message errorMsg = 
            new org.apache.cxf.common.i18n.Message(name, 
                                                   BUNDLE,
                                                   message.get(Message.REQUEST_URI),
                                                   getCurrentPath(firstCri.getValue()),
                                                   httpMethod,
                                                   mediaTypeToString(requestType),
                                                   convertTypesToString(acceptContentTypes));
        if (!"OPTIONS".equalsIgnoreCase(httpMethod)) {
            LOG.warning(errorMsg.toString());
        }
        Response response = 
            createResponse(getRootResources(message), message, errorMsg.toString(), status, methodMatched == 0);
        throw ExceptionUtils.toHttpException(null, response);
        
    }    

    private static List<MediaType> intersectSortMediaTypes(List<MediaType> acceptTypes,
                                                           List<MediaType> producesTypes,
                                                           final boolean checkDistance) {
        List<MediaType> all = intersectMimeTypes(acceptTypes, producesTypes, true, checkDistance);
        if (all.size() > 1) {
            Collections.sort(all, new Comparator<MediaType>() {

                public int compare(MediaType mt1, MediaType mt2) {
                    int result = compareMediaTypes(mt1, mt2, null);
                    if (result == 0) {
                        result = compareQualityAndDistance(mt1, mt2, checkDistance);
                    }
                    return result;
                }
                
            });    
        }
        return all;
    }
    
    private static int compareQualityAndDistance(MediaType mt1, MediaType mt2, boolean checkDistance) {
        int result = compareMediaTypesQualityFactors(mt1, mt2, MEDIA_TYPE_Q_PARAM);
        if (result == 0) {
            result = compareMediaTypesQualityFactors(mt1, mt2, MEDIA_TYPE_QS_PARAM);
        }
        if (result == 0 && checkDistance) {
            Integer dist1 = Integer.valueOf(mt1.getParameters().get(MEDIA_TYPE_DISTANCE_PARAM));
            Integer dist2 = Integer.valueOf(mt2.getParameters().get(MEDIA_TYPE_DISTANCE_PARAM));
            result = dist1.compareTo(dist2);
        }
        return result;
    }
    
    private static String getCurrentPath(MultivaluedMap<String, String> values) {
        String path = values.getFirst(URITemplate.FINAL_MATCH_GROUP);
        return path == null ?  "/" : path;
    }
    
    public static List<ClassResourceInfo> getRootResources(Message message) {
        Service service = message.getExchange().getService();
        return ((JAXRSServiceImpl)service).getClassResourceInfos();
    }
    
    public static boolean noResourceMethodForOptions(Response exResponse, String httpMethod) {
        return exResponse != null && exResponse.getStatus() == 405 
            && "OPTIONS".equalsIgnoreCase(httpMethod);
    }
    
    private static void logNoMatchMessage(OperationResourceInfo ori, 
        String path, String httpMethod, MediaType requestType, List<MediaType> acceptContentTypes) {
        org.apache.cxf.common.i18n.Message errorMsg = 
            new org.apache.cxf.common.i18n.Message("OPER_NO_MATCH", 
                                                   BUNDLE,
                                                   ori.getMethodToInvoke().getName(),
                                                   path,
                                                   ori.getURITemplate().getValue(),
                                                   httpMethod,
                                                   ori.getHttpMethod(),
                                                   requestType.toString(),
                                                   convertTypesToString(ori.getConsumeTypes()),
                                                   convertTypesToString(acceptContentTypes),
                                                   convertTypesToString(ori.getProduceTypes()));
        LOG.fine(errorMsg.toString());
    }

    public static Response createResponse(List<ClassResourceInfo> cris, Message msg,
                                          String responseMessage, int status, boolean addAllow) {
        ResponseBuilder rb = toResponseBuilder(status);
        if (addAllow) {
            Set<String> allowedMethods = new HashSet<String>();
            for (ClassResourceInfo cri : cris) {
                allowedMethods.addAll(cri.getAllowedMethods());
            }
            
            for (String m : allowedMethods) {
                rb.header("Allow", m);
            }
            // "OPTIONS" are supported all the time really
            if (!allowedMethods.contains("OPTIONS")) {
                rb.header("Allow", "OPTIONS");
            }
            if (!allowedMethods.contains("HEAD") && allowedMethods.contains("GET")) {
                rb.header("Allow", "HEAD");
            }
        }
        if (msg != null && MessageUtils.isTrue(msg.getContextualProperty(REPORT_FAULT_MESSAGE_PROPERTY))) {
            rb.type(MediaType.TEXT_PLAIN_TYPE).entity(responseMessage);
        }
        return rb.build();
    }
    
    private static boolean matchHttpMethod(String expectedMethod, String httpMethod) {
        return expectedMethod.equalsIgnoreCase(httpMethod) 
            || headMethodPossible(expectedMethod, httpMethod)
            || expectedMethod.equals(DefaultMethod.class.getSimpleName());
    }
    
    public static boolean headMethodPossible(String expectedMethod, String httpMethod) {
        return HttpMethod.HEAD.equalsIgnoreCase(httpMethod) && HttpMethod.GET.equals(expectedMethod);        
    }
    
    private static String convertTypesToString(List<MediaType> types) {
        StringBuilder sb = new StringBuilder();
        for (MediaType type : types) {
            sb.append(mediaTypeToString(type)).append(',');
        }
        return sb.toString();
    }
    
    public static List<MediaType> getConsumeTypes(Consumes cm) {
        return cm == null ? Collections.singletonList(ALL_TYPES)
                          : getMediaTypes(cm.value());
    }
    
    public static List<MediaType> getProduceTypes(Produces pm) {
        return pm == null ? Collections.singletonList(ALL_TYPES)
                          : getMediaTypes(pm.value());
    }
    
    public static int compareSortedConsumesMediaTypes(List<MediaType> mts1, List<MediaType> mts2, MediaType ct) {
        List<MediaType> actualMts1 = getCompatibleMediaTypes(mts1, ct);
        List<MediaType> actualMts2 = getCompatibleMediaTypes(mts2, ct);
        return compareSortedMediaTypes(actualMts1, actualMts2, null);
    }
    
    public static int compareSortedAcceptMediaTypes(List<MediaType> mts1, List<MediaType> mts2, 
                                                    List<MediaType> acceptTypes) {
        List<MediaType> actualMts1 = intersectSortMediaTypes(mts1, acceptTypes, true);
        List<MediaType> actualMts2 = intersectSortMediaTypes(mts2, acceptTypes, true);
        int size1 = actualMts1.size();
        int size2 = actualMts2.size();
        for (int i = 0; i < size1 && i < size2; i++) {
            int result = compareMediaTypes(actualMts1.get(i), actualMts2.get(i), null);
            if (result == 0) {
                result = compareQualityAndDistance(actualMts1.get(i), actualMts2.get(i), true);
            }
            if (result != 0) {
                return result;
            }
        }
        return size1 == size2 ? 0 : size1 < size2 ? -1 : 1;
    }
    
    private static List<MediaType> getCompatibleMediaTypes(List<MediaType> mts, MediaType ct) {
        List<MediaType> actualMts;
        if (mts.size() == 1) {
            actualMts = mts;
        } else {
            actualMts = new LinkedList<MediaType>();
            for (MediaType mt : mts) {
                if (isMediaTypeCompatible(mt, ct)) {
                    actualMts.add(mt);    
                }
            }
        }
        return actualMts;
    }
    
    public static int compareSortedMediaTypes(List<MediaType> mts1, List<MediaType> mts2, String qs) {
        int size1 = mts1.size();
        int size2 = mts2.size();
        for (int i = 0; i < size1 && i < size2; i++) {
            int result = compareMediaTypes(mts1.get(i), mts2.get(i), qs);
            if (result != 0) {
                return result;
            }
        }
        return size1 == size2 ? 0 : size1 < size2 ? -1 : 1;
    }
    public static int compareMediaTypes(MediaType mt1, MediaType mt2) {
        return compareMediaTypes(mt1, mt2, MEDIA_TYPE_Q_PARAM);
    }
    public static int compareMediaTypes(MediaType mt1, MediaType mt2, String qs) {
        
        boolean mt1TypeWildcard = mt1.isWildcardType();
        boolean mt2TypeWildcard = mt2.isWildcardType();
        if (mt1TypeWildcard && !mt2TypeWildcard) {
            return 1;
        }
        if (!mt1TypeWildcard && mt2TypeWildcard) {
            return -1;
        }
         
        boolean mt1SubTypeWildcard = mt1.getSubtype().contains(MediaType.MEDIA_TYPE_WILDCARD);
        boolean mt2SubTypeWildcard = mt2.getSubtype().contains(MediaType.MEDIA_TYPE_WILDCARD);
        if (mt1SubTypeWildcard && !mt2SubTypeWildcard) {
            return 1;
        }
        if (!mt1SubTypeWildcard && mt2SubTypeWildcard) {
            return -1;
        }       
        
        return qs != null ? compareMediaTypesQualityFactors(mt1, mt2, qs) : 0;
    }
    
    public static int compareMediaTypesQualityFactors(MediaType mt1, MediaType mt2) {
        float q1 = getMediaTypeQualityFactor(mt1.getParameters().get(MEDIA_TYPE_Q_PARAM));
        float q2 = getMediaTypeQualityFactor(mt2.getParameters().get(MEDIA_TYPE_Q_PARAM));
        return Float.compare(q1, q2) * -1;
    }
    
    public static int compareMediaTypesQualityFactors(MediaType mt1, MediaType mt2, String qs) {
        float q1 = getMediaTypeQualityFactor(mt1.getParameters().get(qs));
        float q2 = getMediaTypeQualityFactor(mt2.getParameters().get(qs));
        return Float.compare(q1, q2) * -1;
    }

    public static float getMediaTypeQualityFactor(String q) {
        if (q == null) {
            return 1;
        }
        if (q.charAt(0) == '.') {
            q = '0' + q;
        }
        try {
            return Float.parseFloat(q);
        } catch (NumberFormatException ex) {
            // default value will do
        }
        return 1;
    }
    
    //Message contains following information: PATH, HTTP_REQUEST_METHOD, CONTENT_TYPE, InputStream.
    public static List<Object> processParameters(OperationResourceInfo ori, 
                                                 MultivaluedMap<String, String> values, 
                                                 Message message)
        throws IOException, WebApplicationException {
        
        Class<?>[] parameterTypes = ori.getInParameterTypes();
        List<Parameter> paramsInfo = ori.getParameters();  
        boolean preferModelParams = paramsInfo.size() > parameterTypes.length 
            && !PropertyUtils.isTrue(message.getContextualProperty("org.apache.cxf.preferMethodParameters"));
        
        int parameterTypesLengh = preferModelParams ? paramsInfo.size() : parameterTypes.length;
        
        Type[] genericParameterTypes = ori.getInGenericParameterTypes();
        Annotation[][] anns = ori.getInParameterAnnotations();
        List<Object> params = new ArrayList<Object>(parameterTypesLengh);

        for (int i = 0; i < parameterTypesLengh; i++) {
            Class<?> param = null;
            Type genericParam = null;
            Annotation[] paramAnns = null;
            if (!preferModelParams) {
                param = parameterTypes[i]; 
                genericParam = InjectionUtils.processGenericTypeIfNeeded(
                    ori.getClassResourceInfo().getServiceClass(), param, genericParameterTypes[i]);
                param = InjectionUtils.updateParamClassToTypeIfNeeded(param, genericParam);
                paramAnns = anns == null ? EMPTY_ANNOTATIONS : anns[i];
            } else {
                param = paramsInfo.get(i).getJavaType();
                genericParam = param;
                paramAnns = EMPTY_ANNOTATIONS;
            }
            
            Object paramValue = processParameter(param, 
                                                 genericParam,
                                                 paramAnns,
                                                 paramsInfo.get(i), 
                                                 values, 
                                                 message,
                                                 ori);
            params.add(paramValue);
        }

        return params;
    }

    private static Object processParameter(Class<?> parameterClass, 
                                           Type parameterType,
                                           Annotation[] parameterAnns,
                                           Parameter parameter, 
                                           MultivaluedMap<String, String> values,
                                           Message message,
                                           OperationResourceInfo ori) 
        throws IOException, WebApplicationException {
        InputStream is = message.getContent(InputStream.class);

        if (parameter.getType() == ParameterType.REQUEST_BODY) {
            
            if (parameterClass == AsyncResponse.class) {
                return new AsyncResponseImpl(message);
            }
            
            String contentType = (String)message.get(Message.CONTENT_TYPE);

            if (contentType == null) {
                String defaultCt = (String)message.getContextualProperty(DEFAULT_CONTENT_TYPE);
                contentType = defaultCt == null ? MediaType.APPLICATION_OCTET_STREAM : defaultCt;
            }

            return readFromMessageBody(parameterClass,
                                       parameterType,
                                       parameterAnns,
                                       is, 
                                       toMediaType(contentType),
                                       ori,
                                       message);
        } else if (parameter.getType() == ParameterType.CONTEXT) {
            return createContextValue(message, parameterType, parameterClass);
        } else if (parameter.getType() == ParameterType.BEAN) {
            return createBeanParamValue(message, parameterClass, ori);
        } else {
            
            return createHttpParameterValue(parameter,
                                            parameterClass,
                                            parameterType,
                                            parameterAnns,
                                            message,
                                            values,
                                            ori);
        }
    }
    
    public static Object createHttpParameterValue(Parameter parameter, 
                                            Class<?> parameterClass, 
                                            Type genericParam,
                                            Annotation[] paramAnns,
                                            Message message,
                                            MultivaluedMap<String, String> values,
                                            OperationResourceInfo ori) {
       
        boolean isEncoded = parameter.isEncoded() || ori != null && ori.isEncodedEnabled();
        String defaultValue = parameter.getDefaultValue();
        if (defaultValue == null && ori != null) {
            defaultValue = ori.getDefaultParameterValue();
        }
        
        if (parameter.getType() == ParameterType.PATH) {
            return readFromUriParam(message, parameter.getName(), parameterClass, genericParam,
                                      paramAnns, values, defaultValue, !isEncoded);
        } 
        
        if (parameter.getType() == ParameterType.QUERY) {
            return readQueryString(parameter.getName(), parameterClass, genericParam, 
                                     paramAnns, message, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.MATRIX) {
            return processMatrixParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.FORM) {
            return processFormParam(message, parameter.getName(), parameterClass, genericParam, 
                                      paramAnns, defaultValue, !isEncoded);
        }
        
        if (parameter.getType() == ParameterType.COOKIE) {
            return processCookieParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue);
        } 
        
        Object result = null;
        if (parameter.getType() == ParameterType.HEADER) {
            result = processHeaderParam(message, parameter.getName(), parameterClass, genericParam,
                                        paramAnns, defaultValue);
        }
        return result;
    }
    
    private static Object processMatrixParam(Message m, String key, 
                                             Class<?> pClass, Type genericType,
                                             Annotation[] paramAnns,
                                             String defaultValue,
                                             boolean decode) {
        List<PathSegment> segments = JAXRSUtils.getPathSegments(
                                      (String)m.get(Message.REQUEST_URI), decode);
        if (segments.size() > 0) {
            MultivaluedMap<String, String> params = new MetadataMap<String, String>(); 
            for (PathSegment ps : segments) {
                MultivaluedMap<String, String> matrix = ps.getMatrixParameters();
                for (Map.Entry<String, List<String>> entry : matrix.entrySet()) {
                    for (String value : entry.getValue()) {                    
                        params.add(entry.getKey(), value);
                    }
                }
            }
            
            if ("".equals(key)) {
                return InjectionUtils.handleBean(pClass, paramAnns, params, ParameterType.MATRIX, m, false);
            } else {
                List<String> values = params.get(key);
                return InjectionUtils.createParameterObject(values, 
                                                            pClass, 
                                                            genericType,
                                                            paramAnns, 
                                                            defaultValue,
                                                            false,
                                                            ParameterType.MATRIX,
                                                            m);
            }
        }
        
        return null;
    }
    
    private static Object processFormParam(Message m, String key, 
                                           Class<?> pClass, Type genericType,
                                           Annotation[] paramAnns,
                                           String defaultValue,
                                           boolean decode) {
        
        MessageContext mc = new MessageContextImpl(m);
        MediaType mt = mc.getHttpHeaders().getMediaType();
        
        @SuppressWarnings("unchecked")
        MultivaluedMap<String, String> params = 
            (MultivaluedMap<String, String>)m.get(FormUtils.FORM_PARAM_MAP); 
        
        if (params == null) {
            params = new MetadataMap<String, String>();
            m.put(FormUtils.FORM_PARAM_MAP, params);
        
            if (mt == null || mt.isCompatible(MediaType.APPLICATION_FORM_URLENCODED_TYPE)) {
                String enc = HttpUtils.getEncoding(mt, StandardCharsets.UTF_8.name());
                String body = FormUtils.readBody(m.getContent(InputStream.class), enc);
                FormUtils.populateMapFromStringOrHttpRequest(params, m, body, enc, decode);
            } else {
                if ("multipart".equalsIgnoreCase(mt.getType()) 
                    && MediaType.MULTIPART_FORM_DATA_TYPE.isCompatible(mt)) {
                    MultipartBody body = AttachmentUtils.getMultipartBody(mc);
                    FormUtils.populateMapFromMultipart(params, body, m, decode);
                } else {
                    org.apache.cxf.common.i18n.Message errorMsg = 
                        new org.apache.cxf.common.i18n.Message("WRONG_FORM_MEDIA_TYPE", 
                                                               BUNDLE, 
                                                               mt.toString());
                    LOG.warning(errorMsg.toString());
                    throw ExceptionUtils.toNotSupportedException(null, null);
                }
            }
        }
        
        if ("".equals(key)) {
            return InjectionUtils.handleBean(pClass, paramAnns, params, ParameterType.FORM, m, false);
        } else {
            List<String> results = params.get(key);
    
            return InjectionUtils.createParameterObject(results, 
                                                        pClass, 
                                                        genericType,
                                                        paramAnns,
                                                        defaultValue,
                                                        false,
                                                        ParameterType.FORM,
                                                        m);
             
        }
    }
    
    
    public static MultivaluedMap<String, String> getMatrixParams(String path, boolean decode) {
        int index = path.indexOf(';');
        return index == -1 ? new MetadataMap<String, String>()
                           : JAXRSUtils.getStructuredParams(path.substring(index + 1), ";", decode, false);
    }
    
    private static Object processHeaderParam(Message m, 
                                             String header, 
                                             Class<?> pClass,
                                             Type genericType, 
                                             Annotation[] paramAnns,
                                             String defaultValue) {
        
        List<String> values = new HttpHeadersImpl(m).getRequestHeader(header);
        if (values != null && values.isEmpty()) {
            values = null;
        }
        return InjectionUtils.createParameterObject(values, 
                                                    pClass, 
                                                    genericType,
                                                    paramAnns,
                                                    defaultValue,
                                                    false,
                                                    ParameterType.HEADER,
                                                    m);
             
        
    }

    private static Object processCookieParam(Message m, String cookieName, 
                              Class<?> pClass, Type genericType, 
                              Annotation[] paramAnns, String defaultValue) {
        Cookie c = new HttpHeadersImpl(m).getCookies().get(cookieName);
        
        if (c == null && defaultValue != null) {
            c = Cookie.valueOf(cookieName + '=' + defaultValue);
        }
        if (c == null) {
            return null;
        }
        
        if (pClass.isAssignableFrom(Cookie.class)) {
            return c;
        }
        String value = InjectionUtils.isSupportedCollectionOrArray(pClass) 
            && InjectionUtils.getActualType(genericType) == Cookie.class
            ? c.toString() : c.getValue();
        return InjectionUtils.createParameterObject(Collections.singletonList(value), 
                                                    pClass, 
                                                    genericType,
                                                    paramAnns,
                                                    null,
                                                    false,
                                                    ParameterType.COOKIE,
                                                    m);
    }
    
    public static Object createBeanParamValue(Message m, Class<?> clazz, OperationResourceInfo ori) {
        BeanParamInfo bmi = ServerProviderFactory.getInstance(m).getBeanParamInfo(clazz);
        if (bmi == null) {
            // we could've started introspecting now but the fact no bean info 
            // is available indicates that the one created at start up has been 
            // lost and hence it is 500
            LOG.warning("Bean parameter info is not available");
            throw ExceptionUtils.toInternalServerErrorException(null, null);
        }
        Object instance;
        try {
            instance = clazz.newInstance();
        } catch (Throwable t) {
            throw ExceptionUtils.toInternalServerErrorException(t, null); 
        }
        JAXRSUtils.injectParameters(ori, bmi, instance, m);
        
        InjectionUtils.injectContexts(instance, bmi, m);
        
        return instance;
    }
    
    public static Message getContextMessage(Message m) {
        
        Message contextMessage = m.getExchange() != null ? m.getExchange().getInMessage() : m;
        if (contextMessage == null && !PropertyUtils.isTrue(m.get(Message.INBOUND_MESSAGE))) {
            contextMessage = m;
        }
        return contextMessage;
    }
    
    public static <T> T createContextValue(Message m, Type genericType, Class<T> clazz) {
 
        Message contextMessage = getContextMessage(m);
        Object o = null;
        if (UriInfo.class.isAssignableFrom(clazz)) {
            o = createUriInfo(contextMessage);
        } else if (HttpHeaders.class.isAssignableFrom(clazz)
            || ProtocolHeaders.class.isAssignableFrom(clazz)) {
            o = createHttpHeaders(contextMessage, clazz);
        } else if (SecurityContext.class.isAssignableFrom(clazz)) {
            SecurityContext customContext = contextMessage.get(SecurityContext.class);
            o = customContext == null ? new SecurityContextImpl(contextMessage) : customContext;
        } else if (MessageContext.class.isAssignableFrom(clazz)) {
            o = new MessageContextImpl(m);
        } else if (ResourceInfo.class.isAssignableFrom(clazz)) {
            o = new ResourceInfoImpl(contextMessage);
        } else if (ResourceContext.class.isAssignableFrom(clazz)) {
            o = new ResourceContextImpl(contextMessage, contextMessage.getExchange().get(OperationResourceInfo.class));
        } else if (Request.class.isAssignableFrom(clazz)) {
            o = new RequestImpl(contextMessage);
        } else if (Providers.class.isAssignableFrom(clazz)) {
            o = new ProvidersImpl(contextMessage);
        } else if (ContextResolver.class.isAssignableFrom(clazz)) {
            o = createContextResolver(genericType, contextMessage);
        } else if (Configuration.class.isAssignableFrom(clazz)) {
            o = ProviderFactory.getInstance(contextMessage).getConfiguration(contextMessage);
        } else if (Application.class.isAssignableFrom(clazz)) {
            ProviderInfo<?> providerInfo = 
                (ProviderInfo<?>)contextMessage.getExchange().getEndpoint().get(Application.class.getName());
            o = providerInfo == null ? null : providerInfo.getProvider();
        } else if (contextMessage != null) {
            ContextProvider<?> provider = 
                ProviderFactory.getInstance(contextMessage).createContextProvider(clazz, contextMessage);
            if (provider != null) {
                o = provider.createContext(contextMessage);
            }
        }
        if (o == null && contextMessage != null && !MessageUtils.isRequestor(contextMessage)) {
            o = HttpUtils.createServletResourceValue(contextMessage, clazz);
        }
        return clazz.cast(o);
    }
    
    @SuppressWarnings("unchecked")
    private static UriInfo createUriInfo(Message m) {
        if (MessageUtils.isRequestor(m)) {
            m = m.getExchange() != null ? m.getExchange().getOutMessage() : m;
        }
        MultivaluedMap<String, String> templateParams =
            (MultivaluedMap<String, String>)m.get(URITemplate.TEMPLATE_PARAMETERS);
        return new UriInfoImpl(m, templateParams);
    }
    
    private static Object createHttpHeaders(Message m, Class<?> ctxClass) {
        if (MessageUtils.isRequestor(m)) {
            m = m.getExchange() != null ? m.getExchange().getOutMessage() : m;
        }
        return HttpHeaders.class.isAssignableFrom(ctxClass) ? new HttpHeadersImpl(m)
            : new ProtocolHeadersImpl(m);
    }
    
    public static ContextResolver<?> createContextResolver(Type genericType, Message m) {
        if (genericType instanceof ParameterizedType) {
            return ProviderFactory.getInstance(m).createContextResolver(
                      ((ParameterizedType)genericType).getActualTypeArguments()[0], m);
        } else if (m != null) {
            return ProviderFactory.getInstance(m).createContextResolver(genericType, m);
        } else {
            return null;
        }
    }

    public static Object createResourceValue(Message m, Type genericType, Class<?> clazz) {
                
        // lets assume we're aware of servlet types only that can be @Resource-annotated
        return createContextValue(m, genericType, clazz);
    }
    //CHECKSTYLE:OFF
    private static Object readFromUriParam(Message m,
                                           String parameterName,
                                           Class<?> paramType,
                                           Type genericType,
                                           Annotation[] paramAnns,
                                           MultivaluedMap<String, String> values,
                                           String defaultValue,
                                           boolean decoded) {
    //CHECKSTYLE:ON    
        if ("".equals(parameterName)) {
            return InjectionUtils.handleBean(paramType, paramAnns, values, ParameterType.PATH, m, decoded);
        } else {
            List<String> results = values.get(parameterName);
            return InjectionUtils.createParameterObject(results, 
                                                    paramType, 
                                                    genericType,
                                                    paramAnns,
                                                    defaultValue,
                                                    decoded,
                                                    ParameterType.PATH,
                                                    m);
        }
    }
    
    
    
    //TODO : multiple query string parsing, do it once
    private static Object readQueryString(String queryName,
                                          Class<?> paramType,
                                          Type genericType,
                                          Annotation[] paramAnns,
                                          Message m, 
                                          String defaultValue,
                                          boolean decode) {
        
        MultivaluedMap<String, String> queryMap = new UriInfoImpl(m, null).getQueryParameters(decode);
        
        if ("".equals(queryName)) {
            return InjectionUtils.handleBean(paramType, paramAnns, queryMap, ParameterType.QUERY, m, false);
        } else {
            return InjectionUtils.createParameterObject(queryMap.get(queryName), 
                                                        paramType, 
                                                        genericType,
                                                        paramAnns,
                                                        defaultValue,
                                                        false,
                                                        ParameterType.QUERY, m);
        }
    }

    
    
    /**
     * Retrieve map of query parameters from the passed in message
     * @param message
     * @return a Map of query parameters.
     */
    public static MultivaluedMap<String, String> getStructuredParams(String query, 
                                                                    String sep, 
                                                                    boolean decode,
                                                                    boolean decodePlus) {
        MultivaluedMap<String, String> map = 
            new MetadataMap<String, String>(new LinkedHashMap<String, List<String>>());
        
        getStructuredParams(map, query, sep, decode, decodePlus);
        
        return map;
    }
    
    public static void getStructuredParams(MultivaluedMap<String, String> queries,
                                           String query, 
                                           String sep, 
                                           boolean decode,
                                           boolean decodePlus) {
        getStructuredParams(queries, query, sep, decode, decodePlus, false);
    }
        
    public static void getStructuredParams(MultivaluedMap<String, String> queries,
                                           String query, 
                                           String sep, 
                                           boolean decode,
                                           boolean decodePlus,
                                           boolean valueIsCollection) {    
        if (!StringUtils.isEmpty(query)) {            
            List<String> parts = Arrays.asList(StringUtils.split(query, sep));
            for (String part : parts) {
                int index = part.indexOf('=');
                String name = null;
                String value = null;
                if (index == -1) {
                    name = part;
                    value = "";
                } else {
                    name = part.substring(0, index);
                    value =  index < part.length() ? part.substring(index + 1) : "";
                }
                if (valueIsCollection) {
                    for (String s : value.split(",")) {
                        addStructuredPartToMap(queries, sep, name, s, decode, decodePlus);
                    }
                } else {
                    addStructuredPartToMap(queries, sep, name, value, decode, decodePlus);
                }
            }
        }
    }
    
    private static void addStructuredPartToMap(MultivaluedMap<String, String> queries,
                                               String sep, 
                                               String name,
                                               String value,
                                               boolean decode,
                                               boolean decodePlus) {    
        
        if (decodePlus && value.contains("+")) {
            value = value.replace('+', ' ');
        }
        if (decode) {
            value = (";".equals(sep))
                ? HttpUtils.pathDecode(value) : HttpUtils.urlDecode(value); 
        }
        
        queries.add(HttpUtils.urlDecode(name), value);
    }

    private static Object readFromMessageBody(Class<?> targetTypeClass,
                                                  Type parameterType,
                                                  Annotation[] parameterAnnotations,
                                                  InputStream is, 
                                                  MediaType contentType, 
                                                  OperationResourceInfo ori,
                                                  Message m) throws IOException, WebApplicationException {
        
        List<MediaType> types = JAXRSUtils.intersectMimeTypes(ori.getConsumeTypes(), contentType);
        
        final ProviderFactory pf = ServerProviderFactory.getInstance(m);
        for (MediaType type : types) { 
            List<ReaderInterceptor> readers = pf.createMessageBodyReaderInterceptor(
                                         targetTypeClass,
                                         parameterType,
                                         parameterAnnotations,
                                         type,
                                         m,
                                         true,
                                         ori.getNameBindings());
            if (readers != null) {
                try {
                    return readFromMessageBodyReader(readers, 
                                                     targetTypeClass, 
                                                     parameterType, 
                                                     parameterAnnotations, 
                                                     is, 
                                                     type,
                                                     m);    
                } catch (IOException e) {
                    if (e.getClass().getName().equals(NO_CONTENT_EXCEPTION)) {
                        throw ExceptionUtils.toBadRequestException(e, null);    
                    } else {
                        throw e;
                    }
                } catch (WebApplicationException ex) {
                    throw ex;
                } catch (Exception ex) {
                    throw new Fault(ex);
                }
            }
        }

        logMessageHandlerProblem("NO_MSG_READER", targetTypeClass, contentType);
        throw new WebApplicationException(Response.Status.UNSUPPORTED_MEDIA_TYPE);
    }
    
    @SuppressWarnings("unchecked")
    public static Object readFromMessageBodyReader(List<ReaderInterceptor> readers,
                                                   Class<?> targetTypeClass,
                                                   Type parameterType,
                                                   Annotation[] parameterAnnotations,
                                                   InputStream is, 
                                                   MediaType mediaType, 
                                                   Message m) throws IOException, WebApplicationException {
        
        // Verbose but avoids an extra context instantiation for the typical path
        if (readers.size() > 1) {
            ReaderInterceptor first = readers.remove(0);
            ReaderInterceptorContext context = new ReaderInterceptorContextImpl(targetTypeClass, 
                                                                            parameterType, 
                                                                            parameterAnnotations, 
                                                                            is,
                                                                            m,
                                                                            readers);
            
            return first.aroundReadFrom(context);
        } else {
            MessageBodyReader<?> provider = ((ReaderInterceptorMBR)readers.get(0)).getMBR();
            @SuppressWarnings("rawtypes")
            Class cls = (Class)targetTypeClass;
            return provider.readFrom(
                      cls, parameterType, parameterAnnotations, mediaType,
                      new HttpHeadersImpl(m).getRequestHeaders(), is);
        }
    }

    
    //CHECKSTYLE:OFF
    public static void writeMessageBody(List<WriterInterceptor> writers, 
                                Object entity,
                                Class<?> type, Type genericType,
                                Annotation[] annotations, 
                                MediaType mediaType,
                                MultivaluedMap<String, Object> httpHeaders,
                                Message message) 
        throws WebApplicationException, IOException {
        
        OutputStream entityStream = message.getContent(OutputStream.class);
        if (writers.size() > 1) {
            WriterInterceptor first = writers.remove(0);
            WriterInterceptorContext context = new WriterInterceptorContextImpl(entity,
                                                                                type, 
                                                                            genericType, 
                                                                            annotations,
                                                                            entityStream,
                                                                            message,
                                                                            writers);
            
            first.aroundWriteTo(context);
        } else {
            MessageBodyWriter<Object> writer = ((WriterInterceptorMBW)writers.get(0)).getMBW();
            if (type == byte[].class) {
                long size = writer.getSize(entity, type, genericType, annotations, mediaType);
                if (size != -1) {
                    httpHeaders.putSingle(HttpHeaders.CONTENT_LENGTH, Long.toString(size));
                }
            }
            HttpUtils.convertHeaderValuesToString(httpHeaders, true);
            writer.writeTo(entity, type, genericType, annotations, mediaType,
                           httpHeaders, 
                           entityStream);
        }
    }
    //CHECKSTYLE:ON
    

    public static boolean matchConsumeTypes(MediaType requestContentType, 
                                            OperationResourceInfo ori) {
        
        return !intersectMimeTypes(ori.getConsumeTypes(), requestContentType).isEmpty();
    }
    
    public static boolean matchProduceTypes(MediaType acceptContentType, 
                                              OperationResourceInfo ori) {
        
        return !intersectMimeTypes(ori.getProduceTypes(), acceptContentType).isEmpty();
    }
    
    public static boolean matchMimeTypes(MediaType requestContentType, 
                                         MediaType acceptContentType, 
                                         OperationResourceInfo ori) {
        
        return intersectMimeTypes(ori.getConsumeTypes(), requestContentType).size() != 0
            && intersectMimeTypes(ori.getProduceTypes(), acceptContentType).size() != 0;
    }

    public static List<MediaType> parseMediaTypes(String types) {
        List<MediaType> acceptValues = new ArrayList<MediaType>();
        
        if (types != null) {
            int x = 0;
            int y = types.indexOf(',');
            while (y > 0) {
                acceptValues.add(toMediaType(types.substring(x, y).trim()));
                x = y + 1;
                y = types.indexOf(',', x);
            }
            String lastMediaType = types.substring(x).trim();
            if (!lastMediaType.isEmpty()) {
                acceptValues.add(toMediaType(lastMediaType));
            }
        } else {
            acceptValues.add(ALL_TYPES);
        }
        
        return acceptValues;
    }
    
    /**
     * intersect two mime types
     * 
     * @param mimeTypesA 
     * @param mimeTypesB 
     * @return return a list of intersected mime types
     */   
    public static List<MediaType> intersectMimeTypes(List<MediaType> requiredMediaTypes, 
                                                     List<MediaType> userMediaTypes,
                                                     boolean addRequiredParamsIfPossible) {
        return intersectMimeTypes(requiredMediaTypes, userMediaTypes, addRequiredParamsIfPossible, false);
    }
    public static List<MediaType> intersectMimeTypes(List<MediaType> requiredMediaTypes, 
                                                     List<MediaType> userMediaTypes,
                                                     boolean addRequiredParamsIfPossible,
                                                     boolean addDistanceParameter) {
        Set<MediaType> supportedMimeTypeList = new LinkedHashSet<MediaType>();

        for (MediaType requiredType : requiredMediaTypes) {
            for (MediaType userType : userMediaTypes) {
                boolean isCompatible = isMediaTypeCompatible(requiredType, userType);
                if (isCompatible) {
                    boolean parametersMatched = true;
                    for (Map.Entry<String, String> entry : userType.getParameters().entrySet()) {
                        String value = requiredType.getParameters().get(entry.getKey());
                        if (value != null && entry.getValue() != null
                            && !(stripDoubleQuotesIfNeeded(value).equals(
                                    stripDoubleQuotesIfNeeded(entry.getValue())))) {
                            
                            if (HTTP_CHARSET_PARAM.equals(entry.getKey())
                                && value.equalsIgnoreCase(entry.getValue())) {
                                continue;
                            }
                            parametersMatched = false;
                            break;
                        }
                    }
                    if (!parametersMatched) {
                        continue;
                    }
                    boolean requiredTypeWildcard = requiredType.getType().equals(MediaType.MEDIA_TYPE_WILDCARD);
                    boolean requiredSubTypeWildcard = requiredType.getSubtype().contains(MediaType.MEDIA_TYPE_WILDCARD);
                    
                    String type = requiredTypeWildcard ? userType.getType() : requiredType.getType();
                    String subtype = requiredSubTypeWildcard ? userType.getSubtype() : requiredType.getSubtype();
                    
                    Map<String, String> parameters = userType.getParameters();
                    if (addRequiredParamsIfPossible) {
                        parameters = new LinkedHashMap<String, String>(parameters);
                        for (Map.Entry<String, String> entry : requiredType.getParameters().entrySet()) {
                            if (!parameters.containsKey(entry.getKey())) {
                                parameters.put(entry.getKey(), entry.getValue());
                            }
                        }
                    }
                    if (addDistanceParameter) {
                        int distance = 0;
                        if (requiredTypeWildcard) {
                            distance++;
                        }
                        if (requiredSubTypeWildcard) {
                            distance++;
                        }
                        parameters.put(MEDIA_TYPE_DISTANCE_PARAM, Integer.toString(distance));
                    }
                    supportedMimeTypeList.add(new MediaType(type, subtype, parameters));
                }
            }
        }

        return new ArrayList<MediaType>(supportedMimeTypeList);
        
    }
    
    private static String stripDoubleQuotesIfNeeded(String value) {
        if (value != null && value.startsWith("\"") 
            && value.endsWith("\"") && value.length() > 1) {
            value = value.substring(1,  value.length() - 1);
        }
        return value;
    }

    private static boolean isMediaTypeCompatible(MediaType requiredType, MediaType userType) {
        boolean isCompatible = requiredType.isCompatible(userType);
        if (!isCompatible && requiredType.getType().equalsIgnoreCase(userType.getType())) {
            isCompatible = compareCompositeSubtypes(requiredType, userType,
                                                    PhaseInterceptorChain.getCurrentMessage());
        }
        return isCompatible;
    }
    
    static boolean compareCompositeSubtypes(String requiredType, String userType,
                                            Message message) {
        return compareCompositeSubtypes(toMediaType(requiredType), toMediaType(userType), message);
    }
    
    private static boolean compareCompositeSubtypes(MediaType requiredType, MediaType userType,
                                            Message message) {
        boolean isCompatible = false;
        // check if we have composite subtypes
        String subType1 = requiredType.getSubtype();
        String subType2 = userType.getSubtype();
        
        String subTypeAfterPlus1 = splitMediaSubType(subType1, true); 
        String subTypeAfterPlus2 = splitMediaSubType(subType2, true);
        if (message != null && MessageUtils.isTrue(
            message.getContextualProperty(PARTIAL_HIERARCHICAL_MEDIA_SUBTYPE_CHECK))) {     
            if (subTypeAfterPlus1 != null || subTypeAfterPlus2 != null) {
                boolean nullPossible = subTypeAfterPlus1 == null || subTypeAfterPlus2 == null;
                isCompatible = subTypeAfterPlus1 == null && subTypeAfterPlus2.equals(subType1)
                    || subTypeAfterPlus2 == null && subTypeAfterPlus1.equals(subType2);
                if (!isCompatible && !nullPossible) {
                    isCompatible = subTypeAfterPlus1.equalsIgnoreCase(subTypeAfterPlus2)
                        && (subType1.charAt(0) == '*' || subType2.charAt(0) == '*');
                }
                
                if (!isCompatible) {
                    String subTypeBeforePlus1 = splitMediaSubType(subType1, false);
                    String subTypeBeforePlus2 = splitMediaSubType(subType2, false);
                    nullPossible = subTypeBeforePlus1 == null || subTypeBeforePlus2 == null;
                    isCompatible = subTypeBeforePlus1 == null && subTypeBeforePlus2.equals(subType1)
                        || subTypeBeforePlus2 == null && subTypeBeforePlus1.equals(subType2);
                    if (!isCompatible && !nullPossible) {
                        isCompatible = subTypeBeforePlus1.equalsIgnoreCase(subTypeBeforePlus2)
                            && (subType1.charAt(subType1.length() - 1) == '*' 
                                || subType2.charAt(subType2.length() - 1) == '*');
                    }
                }
            }
        } else {
            if (subTypeAfterPlus1 != null && subTypeAfterPlus2 != null) {
                
                isCompatible = subTypeAfterPlus1.equalsIgnoreCase(subTypeAfterPlus2)
                    && (subType1.charAt(0) == '*' || subType2.charAt(0) == '*');
                
                if (!isCompatible) {
                    String subTypeBeforePlus1 = splitMediaSubType(subType1, false);
                    String subTypeBeforePlus2 = splitMediaSubType(subType2, false);
                    
                    isCompatible = subTypeBeforePlus1.equalsIgnoreCase(subTypeBeforePlus2)
                        && (subType1.charAt(subType1.length() - 1) == '*' 
                            || subType2.charAt(subType2.length() - 1) == '*');
                }
            }
        }
        return isCompatible;
    }
    
    private static String splitMediaSubType(String type, boolean after) {
        int index = type.indexOf('+');
        return index == -1 ? null : after ? type.substring(index + 1) : type.substring(0, index);
    }
    
    public static List<MediaType> intersectMimeTypes(List<MediaType> mimeTypesA, 
                                                     MediaType mimeTypeB) {
        return intersectMimeTypes(mimeTypesA, 
                                  Collections.singletonList(mimeTypeB), false);
    }
    
    public static List<MediaType> intersectMimeTypes(String mimeTypesA, 
                                                     String mimeTypesB) {
        return intersectMimeTypes(parseMediaTypes(mimeTypesA),
                                  parseMediaTypes(mimeTypesB),
                                  false);
    }
    
    public static List<MediaType> sortMediaTypes(String mediaTypes, String qs) {
        return sortMediaTypes(JAXRSUtils.parseMediaTypes(mediaTypes), qs);
    }
    public static List<MediaType> sortMediaTypes(List<MediaType> types, final String qs) {
        if (types.size() > 1) {
            Collections.sort(types, new Comparator<MediaType>() {

                public int compare(MediaType mt1, MediaType mt2) {
                    return JAXRSUtils.compareMediaTypes(mt1, mt2, qs);
                }
                
            });
        }
        return types;
    }
    

    public static <T extends Throwable> Response convertFaultToResponse(T ex, Message currentMessage) {
        return ExceptionUtils.convertFaultToResponse(ex, currentMessage);
    }
    
    public static void setMessageContentType(Message message, Response response) {
        if (response != null) {
            Object ct = response.getMetadata().getFirst(HttpHeaders.CONTENT_TYPE);
            if (ct != null) {
                Exchange ex = message.getExchange();
                if (ex.getInMessage() == message) {
                    ex.put(Message.CONTENT_TYPE, ct.toString());
                } else {
                    message.put(Message.CONTENT_TYPE, ct.toString());
                }
            }
        }
        
    }
    
    public static QName getClassQName(Class<?> type) {
        String nsURI = PackageUtils.getNamespace(PackageUtils.getPackageName(type));
        if (nsURI.endsWith("/")) {
            nsURI = nsURI.substring(0, nsURI.length() - 1);
        }
        return new QName(nsURI, type.getSimpleName(), "ns1"); 
    }

    public static QName convertStringToQName(String name) {
        return DOMUtils.convertStringToQName(name, "");
    }
    
    public static boolean runContainerRequestFilters(ServerProviderFactory pf, 
                                                     Message m, 
                                                     boolean preMatch, 
                                                     Set<String> names) {
        List<ProviderInfo<ContainerRequestFilter>> containerFilters = preMatch 
            ? pf.getPreMatchContainerRequestFilters() : pf.getPostMatchContainerRequestFilters(names);
        if (!containerFilters.isEmpty()) {
            ContainerRequestContext context = new ContainerRequestContextImpl(m, preMatch, false);
            for (ProviderInfo<ContainerRequestFilter> filter : containerFilters) {
                try {
                    InjectionUtils.injectContexts(filter.getProvider(), filter, m);
                    filter.getProvider().filter(context);
                } catch (IOException ex) {
                    throw ExceptionUtils.toInternalServerErrorException(ex, null); 
                }
                Response response = m.getExchange().get(Response.class);
                if (response != null) {
                    setMessageContentType(m, response);
                    return true;
                }
            }
        }
        return false;
    }
    
    public static void runContainerResponseFilters(ServerProviderFactory pf,
                                                   ResponseImpl r,
                                                   Message m, 
                                                   OperationResourceInfo ori,
                                                   Method invoked) throws IOException, Throwable {
        List<ProviderInfo<ContainerResponseFilter>> containerFilters =  
            pf.getContainerResponseFilters(ori == null ? null : ori.getNameBindings());
        if (!containerFilters.isEmpty()) {
            ContainerRequestContext requestContext = 
                new ContainerRequestContextImpl(m.getExchange().getInMessage(), 
                                               false,
                                               true);
            ContainerResponseContext responseContext = 
                new ContainerResponseContextImpl(r, m, 
                    ori == null ? null : ori.getClassResourceInfo().getServiceClass(), invoked);
            for (ProviderInfo<ContainerResponseFilter> filter : containerFilters) {
                InjectionUtils.injectContexts(filter.getProvider(), filter, m);
                filter.getProvider().filter(requestContext, responseContext);
            }
        }
    }
    
    public static String mediaTypeToString(MediaType mt, String... ignoreParams) {
        List<String> list = ignoreParams == null || ignoreParams.length == 0 ? null 
            : Arrays.asList(ignoreParams);
            
        return MediaTypeHeaderProvider.typeToString(mt, list);
    }
    
    public static MediaType toMediaType(String value) {
        if (value == null) {
            return ALL_TYPES;
        } else {
            return MediaTypeHeaderProvider.valueOf(value);
        }
    }
    
    public static Response toResponse(int status) {
        return toResponseBuilder(status).build();
    }
    
    public static Response toResponse(Response.Status status) {
        return toResponse(status.getStatusCode());
    }
    
    public static ResponseBuilder toResponseBuilder(int status) {
        return new ResponseBuilderImpl().status(status);
    }
    
    public static ResponseBuilder toResponseBuilder(Response.Status status) {
        return toResponseBuilder(status.getStatusCode());
    }
    
    public static ResponseBuilder fromResponse(Response response) {
        ResponseBuilder rb = toResponseBuilder(response.getStatus());
        rb.entity(response.getEntity());
        for (Map.Entry<String, List<Object>> entry : response.getMetadata().entrySet()) {
            List<Object> values = entry.getValue();
            for (Object value : values) {
                rb.header(entry.getKey(), value);
            }
        }
        return rb;
    }

    public static Response copyResponseIfNeeded(Response response) {
        if (!(response instanceof ResponseImpl)) {
            Response r = fromResponse(response).build();
            Field[] declaredFields = ReflectionUtil.getDeclaredFields(response.getClass());
            for (Field f : declaredFields) {
                Class<?> declClass = f.getType();
                if (declClass == Annotation[].class) {
                    try {
                        Annotation[] fieldAnnotations = 
                            ReflectionUtil.accessDeclaredField(f, response, Annotation[].class);
                        ((ResponseImpl)r).setEntityAnnotations(fieldAnnotations);
                    } catch (Throwable ex) {
                        LOG.warning("Custom annotations if any can not be copied");
                    }
                    break;
                }
            }
            return r;
        } else {
            return response;
        }
    }
    
    public static Message getCurrentMessage() {
        return PhaseInterceptorChain.getCurrentMessage();
    }
    
    public static ClassResourceInfo getRootResource(Message m) {
        return (ClassResourceInfo)m.getExchange().get(JAXRSUtils.ROOT_RESOURCE_CLASS);
    }
    
    public static void pushOntoStack(OperationResourceInfo ori,
                                     MultivaluedMap<String, String> params,
                                     Message msg) {
        OperationResourceInfoStack stack = msg.get(OperationResourceInfoStack.class);
        if (stack == null) {
            stack = new OperationResourceInfoStack();
            msg.put(OperationResourceInfoStack.class, stack);
        }
        
        
        List<String> values = null;
        if (params.size() <= 1) {
            values = Collections.emptyList();
        } else {
            values = new ArrayList<String>(params.size() - 1);
            addTemplateVarValues(values, params, ori.getClassResourceInfo().getURITemplate());
            addTemplateVarValues(values, params, ori.getURITemplate());
        }
        Class<?> realClass = ori.getClassResourceInfo().getServiceClass();
        stack.push(new MethodInvocationInfo(ori, realClass, values));
    }

    private static void addTemplateVarValues(List<String> values, 
                                             MultivaluedMap<String, String> params,
                                             URITemplate template) {
        if (template != null) {
            for (String var : template.getVariables()) {
                List<String> paramValues = params.get(var);
                if (paramValues != null) {
                    values.addAll(paramValues);
                }
            }
        }
    }

    public static String logMessageHandlerProblem(String name, Class<?> cls, MediaType ct) {
        org.apache.cxf.common.i18n.Message errorMsg = 
            new org.apache.cxf.common.i18n.Message(name, BUNDLE, cls.getName(), mediaTypeToString(ct));
        String errorMessage = errorMsg.toString();
        LOG.severe(errorMessage);
        return errorMessage;
    }
    
}
