package org.jboss.resteasy.spi.metadata;

import static org.jboss.resteasy.spi.util.FindAnnotation.findAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;

import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.Encoded;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.MatrixParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.container.Suspended;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.RuntimeDelegate;

import org.jboss.resteasy.annotations.Body;
import org.jboss.resteasy.annotations.Form;
import org.jboss.resteasy.annotations.Query;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.ResteasyUriBuilder;
import org.jboss.resteasy.spi.util.MethodHashing;
import org.jboss.resteasy.spi.util.PickConstructor;
import org.jboss.resteasy.spi.util.Types;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings(value = "unchecked")
public class ResourceBuilder {
    public static class ResourceClassBuilder {
        final DefaultResourceClass resourceClass;

        List<FieldParameter> fields = new ArrayList<FieldParameter>();

        List<SetterParameter> setters = new ArrayList<SetterParameter>();

        List<ResourceMethod> resourceMethods = new ArrayList<ResourceMethod>();

        List<ResourceLocator> resourceLocators = new ArrayList<ResourceLocator>();

        public ResourceClassBuilder(final Class<?> root, final String path) {
            this.resourceClass = new DefaultResourceClass(root, path);
        }

        public ResourceMethodBuilder method(Method method) {
            return new ResourceMethodBuilder(this, method, method);
        }

        public ResourceMethodBuilder method(Method method, Method annotatedMethod) {
            return new ResourceMethodBuilder(this, method, annotatedMethod);
        }

        public ResourceLocatorBuilder locator(Method method) {
            return new ResourceLocatorBuilder(this, method, method);
        }

        public ResourceLocatorBuilder locator(Method method, Method annotatedMethod) {
            return new ResourceLocatorBuilder(this, method, annotatedMethod);
        }

        public FieldParameterBuilder field(Field field) {
            FieldParameter param = new FieldParameter(resourceClass, field);
            return new FieldParameterBuilder(this, param);
        }

        public SetterParameterBuilder setter(Method method) {
            SetterParameter param = new SetterParameter(resourceClass, method, method);
            return new SetterParameterBuilder(this, param);
        }

        public ResourceConstructorBuilder constructor(Constructor constructor) {
            return new ResourceConstructorBuilder(this, constructor);
        }

        public ResourceClass buildClass() {
            resourceClass.fields = fields.toArray(new FieldParameter[fields.size()]);
            resourceClass.setters = setters.toArray(new SetterParameter[setters.size()]);
            resourceClass.resourceMethods = resourceMethods.toArray(new ResourceMethod[resourceMethods.size()]);
            resourceClass.resourceLocators = resourceLocators.toArray(new ResourceLocator[resourceLocators.size()]);

            return resourceClass;
        }
    }

    public static class ParameterBuilder<T extends ParameterBuilder<T>> {
        final Parameter parameter;

        public ParameterBuilder(final Parameter parameter) {
            this.parameter = parameter;
        }

        public Parameter getParameter() {
            return parameter;
        }

        public T type(Class<?> type) {
            parameter.type = type;
            return (T) this;
        }

        public T genericType(Type type) {
            parameter.genericType = type;
            return (T) this;
        }

        public T type(GenericType type) {
            parameter.type = type.getRawType();
            parameter.genericType = type.getType();
            return (T) this;
        }

        public T beanParam() {
            parameter.paramType = Parameter.ParamType.BEAN_PARAM;
            return (T) this;
        }

        public T context() {
            parameter.paramType = Parameter.ParamType.CONTEXT;
            return (T) this;
        }

        public T messageBody() {
            parameter.paramType = Parameter.ParamType.MESSAGE_BODY;
            return (T) this;
        }

        public T encoded() {
            parameter.encoded = true;
            return (T) this;
        }

        public T defaultValue(String defaultValue) {
            parameter.defaultValue = defaultValue;
            return (T) this;
        }

        public T cookieParam(String name) {
            parameter.paramType = Parameter.ParamType.COOKIE_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        public T formParam(String name) {
            parameter.paramType = Parameter.ParamType.FORM_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        /**
         * Resteasy @Form specific injection parameter.
         *
         * @param prefix prefix
         * @return parameter builder
         */
        public T form(String prefix) {
            parameter.paramType = Parameter.ParamType.FORM;
            parameter.paramName = prefix;
            return (T) this;
        }

        /**
         * Resteasy @Form specific injection parameter.
         *
         * @return parameter builder
         */
        public T form() {
            parameter.paramType = Parameter.ParamType.FORM;
            parameter.paramName = "";
            return (T) this;
        }

        public T headerParam(String name) {
            parameter.paramType = Parameter.ParamType.HEADER_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        public T matrixParam(String name) {
            parameter.paramType = Parameter.ParamType.MATRIX_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        public T pathParam(String name) {
            parameter.paramType = Parameter.ParamType.PATH_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        public T queryParam(String name) {
            parameter.paramType = Parameter.ParamType.QUERY_PARAM;
            parameter.paramName = name;
            return (T) this;
        }

        @SuppressWarnings("deprecation")
        public T fromAnnotations() {
            Annotation[] annotations = parameter.getAnnotations();
            AccessibleObject injectTarget = parameter.getAccessibleObject();
            Class<?> type = parameter.getResourceClass().getClazz();

            parameter.encoded = findAnnotation(annotations, Encoded.class) != null
                    || injectTarget.isAnnotationPresent(Encoded.class) || type.isAnnotationPresent(Encoded.class);
            DefaultValue defaultValue = findAnnotation(annotations, DefaultValue.class);
            if (defaultValue != null)
                parameter.defaultValue = defaultValue.value();

            QueryParam queryParam;
            org.jboss.resteasy.annotations.jaxrs.QueryParam queryParam2;
            Query query;
            HeaderParam header;
            org.jboss.resteasy.annotations.jaxrs.HeaderParam header2;
            MatrixParam matrix;
            org.jboss.resteasy.annotations.jaxrs.MatrixParam matrix2;
            PathParam uriParam;
            org.jboss.resteasy.annotations.jaxrs.PathParam uriParam2;
            CookieParam cookie;
            org.jboss.resteasy.annotations.jaxrs.CookieParam cookie2;
            FormParam formParam;
            org.jboss.resteasy.annotations.jaxrs.FormParam formParam2;
            Form form;
            Suspended suspended;

            if ((queryParam = findAnnotation(annotations, QueryParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.QUERY_PARAM;
                parameter.paramName = queryParam.value();
            } else if ((queryParam2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.QueryParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.QUERY_PARAM;
                if (queryParam2.value() != null && queryParam2.value().length() > 0) {
                    parameter.paramName = queryParam2.value();
                }
            } else if ((query = findAnnotation(annotations, Query.class)) != null) {
                parameter.paramType = Parameter.ParamType.QUERY;
                parameter.paramName = ""; // TODO query.prefix();
            } else if ((header = findAnnotation(annotations, HeaderParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.HEADER_PARAM;
                parameter.paramName = header.value();
            } else if ((header2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.HeaderParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.HEADER_PARAM;
                if (header2.value() != null && header2.value().length() > 0) {
                    parameter.paramName = header2.value();
                }
            } else if ((formParam = findAnnotation(annotations, FormParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.FORM_PARAM;
                parameter.paramName = formParam.value();
            } else if ((formParam2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.FormParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.FORM_PARAM;
                if (formParam2.value() != null && formParam2.value().length() > 0) {
                    parameter.paramName = formParam2.value();
                }
            } else if ((cookie = findAnnotation(annotations, CookieParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.COOKIE_PARAM;
                parameter.paramName = cookie.value();
            } else if ((cookie2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.CookieParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.COOKIE_PARAM;
                if (cookie2.value() != null && cookie2.value().length() > 0) {
                    parameter.paramName = cookie2.value();
                }
            } else if ((uriParam = findAnnotation(annotations, PathParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.PATH_PARAM;
                parameter.paramName = uriParam.value();
            } else if ((uriParam2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.PathParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.PATH_PARAM;
                if (uriParam2.value() != null && uriParam2.value().length() > 0) {
                    parameter.paramName = uriParam2.value();
                }
            } else if ((form = findAnnotation(annotations, Form.class)) != null) {
                parameter.paramType = Parameter.ParamType.FORM;
                parameter.paramName = form.prefix();
            } else if (findAnnotation(annotations, BeanParam.class) != null) {
                parameter.paramType = Parameter.ParamType.BEAN_PARAM;
            } else if ((matrix = findAnnotation(annotations, MatrixParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.MATRIX_PARAM;
                parameter.paramName = matrix.value();
            } else if ((matrix2 = findAnnotation(annotations,
                    org.jboss.resteasy.annotations.jaxrs.MatrixParam.class)) != null) {
                parameter.paramType = Parameter.ParamType.MATRIX_PARAM;
                if (matrix2.value() != null && matrix2.value().length() > 0) {
                    parameter.paramName = matrix2.value();
                }
            } else if (findAnnotation(annotations, Context.class) != null) {
                parameter.paramType = Parameter.ParamType.CONTEXT;
            } else if ((suspended = findAnnotation(annotations, Suspended.class)) != null) {
                parameter.paramType = Parameter.ParamType.SUSPENDED;
            } else if (jakarta.ws.rs.container.AsyncResponse.class.isAssignableFrom(type)) {
                parameter.paramType = Parameter.ParamType.SUSPENDED;
            } else if (findAnnotation(annotations, Body.class) != null) {
                parameter.paramType = Parameter.ParamType.MESSAGE_BODY;
            } else {
                parameter.paramType = Parameter.ParamType.UNKNOWN;
            }
            return (T) this;
        }
    }

    public static class ConstructorParameterBuilder extends ParameterBuilder<ConstructorParameterBuilder> {
        final ResourceConstructorBuilder constructor;

        final ConstructorParameter param;

        public ConstructorParameterBuilder(final ResourceConstructorBuilder builder, final ConstructorParameter param) {
            super(param);
            this.constructor = builder;
            this.param = param;
        }

        public ConstructorParameterBuilder param(int i) {
            return constructor.param(i);
        }

        public ResourceClassBuilder buildConstructor() {
            return constructor.buildConstructor();
        }

    }

    public static class LocatorMethodParameterBuilder<T extends LocatorMethodParameterBuilder<T>>
            extends
            ParameterBuilder<T> {
        final ResourceLocatorBuilder locator;

        final MethodParameter param;

        public LocatorMethodParameterBuilder(final ResourceLocatorBuilder method, final MethodParameter param) {
            super(param);
            this.locator = method;
            this.param = param;
        }

        public T param(int i) {
            return (T) locator.param(i);
        }

        public ResourceClassBuilder buildMethod() {
            return locator.buildMethod();
        }

    }

    public static class ResourceMethodParameterBuilder
            extends
            LocatorMethodParameterBuilder<ResourceMethodParameterBuilder> {
        final ResourceMethodBuilder method;

        public ResourceMethodParameterBuilder(final ResourceMethodBuilder method, final MethodParameter param) {
            super(method, param);
            this.method = method;
        }

        public ResourceMethodParameterBuilder suspended() {
            method.method.asynchronous = true;
            parameter.paramType = Parameter.ParamType.SUSPENDED;
            return this;
        }

        @Override
        public ResourceMethodParameterBuilder fromAnnotations() {
            doFromAnnotations();
            if (param.paramType == Parameter.ParamType.SUSPENDED) {
                method.method.asynchronous = true;
            } else if (param.paramType == Parameter.ParamType.UNKNOWN) {
                param.paramType = Parameter.ParamType.MESSAGE_BODY;
            }
            return this;
        }

        protected void doFromAnnotations() {
            super.fromAnnotations();
        }
    }

    public static class ResourceConstructorBuilder {

        ResourceConstructor constructor;

        ResourceClassBuilder resourceClassBuilder;

        public ResourceConstructorBuilder(final ResourceClassBuilder resourceClassBuilder, final Constructor constructor) {
            this.resourceClassBuilder = resourceClassBuilder;
            this.constructor = new DefaultResourceConstructor(resourceClassBuilder.resourceClass, constructor);
        }

        public ConstructorParameterBuilder param(int i) {
            return new ConstructorParameterBuilder(this, constructor.getParams()[i]);
        }

        public ResourceClassBuilder buildConstructor() {
            resourceClassBuilder.resourceClass.constructor = constructor;
            return resourceClassBuilder;
        }
    }

    public static class ResourceLocatorBuilder<T extends ResourceLocatorBuilder<T>> {

        DefaultResourceLocator locator;

        ResourceClassBuilder resourceClassBuilder;

        ResourceLocatorBuilder() {
        }

        public ResourceLocatorBuilder(final ResourceClassBuilder resourceClassBuilder, final Method method,
                final Method annotatedMethod) {
            this.resourceClassBuilder = resourceClassBuilder;
            this.locator = new DefaultResourceLocator(resourceClassBuilder.resourceClass, method, annotatedMethod);
        }

        public DefaultResourceLocator getLocator() {
            return locator;
        }

        public T returnType(Class<?> type) {
            locator.returnType = type;
            return (T) this;
        }

        public T genericReturnType(Type type) {
            locator.genericReturnType = type;
            return (T) this;
        }

        public T returnType(GenericType type) {
            locator.returnType = type.getRawType();
            locator.genericReturnType = type.getType();
            return (T) this;
        }

        public LocatorMethodParameterBuilder param(int i) {
            return new LocatorMethodParameterBuilder(this, locator.getParams()[i]);
        }

        public ResourceClassBuilder buildMethod() {
            ResteasyUriBuilder builder = (ResteasyUriBuilder) RuntimeDelegate.getInstance().createUriBuilder();
            if (locator.resourceClass.getPath() != null)
                builder.path(locator.resourceClass.getPath());
            if (locator.path != null)
                builder.path(locator.path);
            String pathExpression = builder.getPath();
            if (pathExpression == null)
                pathExpression = "";
            locator.fullpath = pathExpression;
            if (locator.resourceClass.getClazz().isAnonymousClass()) {
                locator.getMethod().setAccessible(true);
            }
            resourceClassBuilder.resourceLocators.add(locator);
            return resourceClassBuilder;
        }

        public T path(String path) {
            locator.path = path;
            return (T) this;
        }
    }

    public static class ResourceMethodBuilder extends ResourceLocatorBuilder<ResourceMethodBuilder> {
        DefaultResourceMethod method;

        public ResourceMethodBuilder(final ResourceClassBuilder resourceClassBuilder, final Method method,
                final Method annotatedMethod) {
            this.method = new DefaultResourceMethod(resourceClassBuilder.resourceClass, method, annotatedMethod);
            this.locator = this.method;
            this.resourceClassBuilder = resourceClassBuilder;
        }

        public ResourceMethodBuilder httpMethod(String httpMethod) {
            method.httpMethods.add(httpMethod.toUpperCase());
            return this;
        }

        public ResourceMethodBuilder get() {
            method.httpMethods.add(HttpMethod.GET);
            return this;
        }

        public ResourceMethodBuilder put() {
            method.httpMethods.add(HttpMethod.PUT);
            return this;
        }

        public ResourceMethodBuilder post() {
            method.httpMethods.add(HttpMethod.POST);
            return this;
        }

        public ResourceMethodBuilder delete() {
            method.httpMethods.add(HttpMethod.DELETE);
            return this;
        }

        public ResourceMethodBuilder options() {
            method.httpMethods.add(HttpMethod.OPTIONS);
            return this;
        }

        public ResourceMethodBuilder head() {
            method.httpMethods.add(HttpMethod.HEAD);
            return this;
        }

        public ResourceMethodBuilder produces(MediaType... produces) {
            method.produces = produces;
            return this;
        }

        public ResourceMethodBuilder produces(String... produces) {
            MediaType[] types = parseMediaTypes(produces);
            method.produces = types;
            for (MediaType mt : types) {
                if (!mt.getParameters().containsKey(MediaType.CHARSET_PARAMETER)) {
                    if (isTextLike(mt)) {
                        ResteasyDeployment deployment = ResteasyProviderFactory.getInstance()
                                .getContextData(ResteasyDeployment.class);
                        if (deployment != null && !deployment.isAddCharset()) {
                            LogMessages.LOGGER.mediaTypeLacksCharset(mt, method.getMethod().getName());
                        }
                    }
                }
            }
            return this;
        }

        private static boolean isTextLike(MediaType mediaType) {
            return "text".equalsIgnoreCase(mediaType.getType()) || ("application".equalsIgnoreCase(mediaType.getType())
                    && mediaType.getSubtype().toLowerCase().startsWith("xml"));
        }

        protected MediaType[] parseMediaTypes(String[] produces) {
            List<MediaType> mediaTypes = new ArrayList<MediaType>();
            for (String produce : produces) {
                String[] split = produce.split(",");
                for (String s : split)
                    mediaTypes.add(MediaType.valueOf(s));
            }
            MediaType[] types = new MediaType[mediaTypes.size()];
            types = mediaTypes.toArray(types);
            return types;
        }

        public ResourceMethodBuilder consumes(MediaType... consumes) {
            method.consumes = consumes;
            return this;
        }

        public ResourceMethodBuilder consumes(String... consumes) {
            MediaType[] types = parseMediaTypes(consumes);
            method.consumes = types;
            return this;
        }

        public ResourceMethodParameterBuilder param(int i) {
            return new ResourceMethodParameterBuilder(this, locator.getParams()[i]);
        }

        public ResourceClassBuilder buildMethod() {
            ResteasyUriBuilder builder = (ResteasyUriBuilder) RuntimeDelegate.getInstance().createUriBuilder();
            if (method.resourceClass.getPath() != null)
                builder.path(method.resourceClass.getPath());
            if (method.path != null)
                builder.path(method.path);
            String pathExpression = builder.getPath();
            if (pathExpression == null)
                pathExpression = "";
            method.fullpath = pathExpression;
            if (method.resourceClass.getClazz().isAnonymousClass()) {
                method.getMethod().setAccessible(true);
            }
            resourceClassBuilder.resourceMethods.add(method);
            return resourceClassBuilder;
        }

        public DefaultResourceMethod getMethod() {
            return method;
        }
    }

    public static class FieldParameterBuilder extends ParameterBuilder<FieldParameterBuilder> {
        FieldParameter field;

        ResourceClassBuilder resourceClassBuilder;

        FieldParameterBuilder(final ResourceClassBuilder resourceClassBuilder, final FieldParameter parameter) {
            super(parameter);
            this.field = parameter;
            this.resourceClassBuilder = resourceClassBuilder;
        }

        public ResourceClassBuilder buildField() {
            field.field.setAccessible(true);
            resourceClassBuilder.fields.add(field);
            return resourceClassBuilder;
        }
    }

    public static class SetterParameterBuilder extends ParameterBuilder<SetterParameterBuilder> {
        SetterParameter setter;

        ResourceClassBuilder resourceClassBuilder;

        SetterParameterBuilder(final ResourceClassBuilder resourceClassBuilder, final SetterParameter parameter) {
            super(parameter);
            this.setter = parameter;
            this.resourceClassBuilder = resourceClassBuilder;
        }

        public ResourceClassBuilder buildSetter() {
            setter.setter.setAccessible(true);
            resourceClassBuilder.setters.add(setter);
            return resourceClassBuilder;
        }
    }

    private final Map<Integer, List<ResourceClassProcessor>> processors = new TreeMap<>(Comparator.reverseOrder());

    /**
     * Register a new {@link ResourceClassProcessor} which will be used to post-process all
     * {@link ResourceClass} instances created from the builder.
     *
     * @param processor resource class processor
     * @param priority  processor priority
     */
    public void registerResourceClassProcessor(ResourceClassProcessor processor, int priority) {
        List<ResourceClassProcessor> l = processors.get(priority);
        if (l == null) {
            l = new LinkedList<ResourceClassProcessor>();
            processors.put(priority, l);
        }
        l.add(processor);
    }

    @Deprecated
    public static ResourceClassBuilder rootResource(Class<?> root) {
        return new ResourceBuilder().buildRootResource(root);
    }

    public ResourceClassBuilder buildRootResource(Class<?> root) {
        return new ResourceClassBuilder(root, "/");
    }

    @Deprecated
    public static ResourceClassBuilder rootResource(Class<?> root, String path) {
        return new ResourceBuilder().buildRootResource(root, path);
    }

    protected ResourceClassBuilder buildRootResource(Class<?> root, String path) {
        return new ResourceClassBuilder(root, path);
    }

    @Deprecated
    public static ResourceClassBuilder locator(Class<?> root) {
        return new ResourceBuilder().buildLocator(root);
    }

    protected ResourceClassBuilder buildLocator(Class<?> root) {
        return new ResourceClassBuilder(root, null);
    }

    @Deprecated
    public static ResourceConstructor constructor(Class<?> annotatedResourceClass) {
        return new ResourceBuilder().getConstructor(annotatedResourceClass);
    }

    public Class<? extends Annotation> getCorrespondingRootAnnotation() {
        return Path.class;
    }

    /**
     * Picks a constructor from an annotated resource class based on spec rules.
     *
     * @param annotatedResourceClass annotated resource class
     * @return {@link ResourceConstructor}
     */
    public ResourceConstructor getConstructor(Class<?> annotatedResourceClass) {
        Constructor constructor = PickConstructor.pickPerRequestConstructor(annotatedResourceClass);
        if (constructor == null) {
            throw new RuntimeException(Messages.MESSAGES.couldNotFindConstructor(annotatedResourceClass.getName()));
        }
        ResourceConstructorBuilder builder = buildRootResource(annotatedResourceClass).constructor(constructor);
        if (constructor.getParameterTypes() != null) {
            for (int i = 0; i < constructor.getParameterCount(); i++)
                builder.param(i).fromAnnotations();
        }
        ResourceClass resourceClass = applyProcessors(builder.buildConstructor().buildClass());
        return resourceClass.getConstructor();
    }

    @Deprecated
    public static ResourceClass rootResourceFromAnnotations(Class<?> clazz) {
        return new ResourceBuilder().getRootResourceFromAnnotations(clazz);
    }

    /**
     * Build metadata from annotations on classes and methods.
     *
     * @param clazz class
     * @return resource class
     */
    public ResourceClass getRootResourceFromAnnotations(Class<?> clazz) {
        return fromAnnotations(false, clazz);
    }

    @Deprecated
    public static ResourceClass locatorFromAnnotations(Class<?> clazz) {
        return new ResourceBuilder().getLocatorFromAnnotations(clazz);
    }

    public ResourceClass getLocatorFromAnnotations(Class<?> clazz) {
        return fromAnnotations(true, clazz);
    }

    private ResourceClass fromAnnotations(boolean isLocator, Class<?> clazz) {
        // stupid hack for Weld as it loses generic type information, but retains annotations.
        if (!clazz.isInterface() && clazz.getSuperclass() != null && !clazz.getSuperclass().equals(Object.class)
                && clazz.isSynthetic()) {
            clazz = clazz.getSuperclass();
        }

        ResourceClassBuilder builder = null;
        if (isLocator)
            builder = buildLocator(clazz);
        else {
            builder = createResourceClassBuilder(clazz);
        }
        for (Method method : clazz.getMethods()) {
            if (!method.isSynthetic() && !method.getDeclaringClass().equals(Object.class))
                processMethod(isLocator, builder, clazz, method);

        }
        if (!clazz.isInterface()) {
            processFields(builder, clazz);
        }
        processSetters(builder, clazz);
        return applyProcessors(builder.buildClass());
    }

    protected ResourceClassBuilder createResourceClassBuilder(Class<?> clazz) {
        ResourceClassBuilder builder;
        Path path = clazz.getAnnotation(Path.class);
        if (path == null)
            builder = buildRootResource(clazz, null);
        else
            builder = buildRootResource(clazz, path.value());
        return builder;
    }

    private static Set<String> getHttpMethods(Method method) {
        HashSet<String> methods = new HashSet<String>();
        for (Annotation annotation : method.getAnnotations()) {
            HttpMethod http = annotation.annotationType().getAnnotation(HttpMethod.class);
            if (http != null)
                methods.add(http.value());
        }
        if (methods.size() == 0)
            return null;
        return methods;
    }

    private static boolean isHttpMethod(Method method) {
        for (Annotation annotation : method.getAnnotations()) {
            HttpMethod http = annotation.annotationType().getAnnotation(HttpMethod.class);
            if (http != null)
                return true;
        }
        return false;
    }

    @Deprecated
    public static Method findAnnotatedMethod(final Class<?> root, final Method implementation) {
        return new ResourceBuilder().getAnnotatedMethod(root, implementation);
    }

    /**
     * Find the annotated resource method or sub-resource method / sub-resource locator in the class hierarchy.
     *
     * @param root           The root resource class.
     * @param implementation The resource method or sub-resource method / sub-resource locator implementation
     * @return The annotated resource method or sub-resource method / sub-resource locator.
     */
    public Method getAnnotatedMethod(final Class<?> root, final Method implementation) {
        if (implementation.isSynthetic()) {
            return null;
        }

        // Check the method itself for JAX-RS annotations
        if (implementation.isAnnotationPresent(Path.class) || isHttpMethod(implementation)) {
            return implementation;
        }

        // Per http://download.oracle.com/auth/otn-pub/jcp/jaxrs-1.0-fr-oth-JSpec/jaxrs-1.0-final-spec.pdf
        // Section 3.2 Annotation Inheritance

        if (implementation.isAnnotationPresent(Produces.class) || implementation.isAnnotationPresent(Consumes.class)) {
            // Abort the search for inherited annotations as specified by the JAX-RS specification.
            // If a implementation method has any JAX-RS annotations then all the annotations
            // on the superclass or interface method are ignored.
            // Therefore a method can be omitted if it is neither a resource method nor a sub-resource method /
            // sub-resource locator but is annotated with other JAX-RS annotations.
            return null;
        }

        // Check super-classes for inherited annotations
        for (Class<?> clazz = implementation.getDeclaringClass().getSuperclass(); clazz != null; clazz = clazz
                .getSuperclass()) {
            final Method overriddenMethod = Types.findOverriddenMethod(implementation.getDeclaringClass(), clazz,
                    implementation);
            if (overriddenMethod == null) {
                continue;
            }

            if (overriddenMethod.isAnnotationPresent(Path.class) || isHttpMethod(overriddenMethod)) {
                return overriddenMethod;
            }
            if (overriddenMethod.isAnnotationPresent(Produces.class)
                    || overriddenMethod.isAnnotationPresent(Consumes.class)) {
                // Abort the search for inherited annotations as specified by the JAX-RS specification.
                // If a implementation method has any JAX-RS annotations then all the annotations
                // on the superclass or interface method are ignored.
                // Therefore a method can be omitted if it is neither a resource method nor a sub-resource method /
                // sub-resource locator but is annotated with other JAX-RS annotations.
                return null;
            }
        }

        // Check implemented interfaces for inherited annotations
        for (Class<?> clazz = root; clazz != null; clazz = clazz.getSuperclass()) {
            Method overriddenMethod = null;

            for (Class<?> classInterface : clazz.getInterfaces()) {
                final Method overriddenInterfaceMethod = Types.getImplementedInterfaceMethod(root, classInterface,
                        implementation);
                if (overriddenInterfaceMethod == null) {
                    continue;
                }
                if (!overriddenInterfaceMethod.isAnnotationPresent(Path.class)
                        && !isHttpMethod(overriddenInterfaceMethod)) {
                    if (overriddenInterfaceMethod.isAnnotationPresent(Produces.class)
                            || overriddenInterfaceMethod.isAnnotationPresent(Consumes.class)) {
                        // Abort the search for inherited annotations as specified by the JAX-RS specification.
                        // If a implementation method has any JAX-RS annotations then all the annotations
                        // on the superclass or interface method are ignored.
                        // Therefore a method can be omitted if it is neither a resource method nor a sub-resource method /
                        // sub-resource locator but is annotated with other JAX-RS annotations.
                        return null;
                    } else {
                        continue;
                    }
                }
                // Ensure no redefinition by peer interfaces (ambiguous) to preserve logic found in
                // original implementation
                if (overriddenMethod != null && !overriddenInterfaceMethod.equals(overriddenMethod)) {
                    throw new RuntimeException(Messages.MESSAGES.ambiguousInheritedAnnotations(implementation));
                }

                overriddenMethod = overriddenInterfaceMethod;
            }

            if (overriddenMethod != null) {
                return overriddenMethod;
            }
        }

        return null;
    }

    protected void processFields(ResourceClassBuilder resourceClassBuilder, Class<?> root) {
        do {
            processDeclaredFields(resourceClassBuilder, root);
            root = root.getSuperclass();
            //      } while (root.getSuperclass() != null && !root.getSuperclass().equals(Object.class));
        } while (root != null && !root.equals(Object.class));
    }

    protected void processSetters(ResourceClassBuilder resourceClassBuilder, Class<?> root) {
        HashSet<Long> hashes = new HashSet<Long>();
        do {
            processDeclaredSetters(resourceClassBuilder, root, hashes);
            root = root.getSuperclass();
        } while (root != null && !root.equals(Object.class));
    }

    protected void processDeclaredFields(ResourceClassBuilder resourceClassBuilder, final Class<?> root) {
        Field[] fieldList = root.getDeclaredFields();

        for (Field field : fieldList) {
            FieldParameterBuilder builder = resourceClassBuilder.field(field).fromAnnotations();
            if (builder.field.paramType == Parameter.ParamType.MESSAGE_BODY && !field.isAnnotationPresent(Body.class))
                continue;
            if (builder.field.paramType == Parameter.ParamType.UNKNOWN)
                continue;
            builder.buildField();
        }
    }

    protected void processDeclaredSetters(ResourceClassBuilder resourceClassBuilder, final Class<?> root,
            Set<Long> visitedHashes) {
        Method[] methodList = root.getDeclaredMethods();

        for (Method method : methodList) {
            if (!method.getName().startsWith("set"))
                continue;
            if (method.getParameterCount() != 1)
                continue;
            long hash = 0;
            try {
                hash = MethodHashing.methodHash(method);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            if (!Modifier.isPrivate(method.getModifiers()) && visitedHashes.contains(hash))
                continue;
            visitedHashes.add(hash);
            SetterParameterBuilder builder = resourceClassBuilder.setter(method).fromAnnotations();
            if (builder.setter.paramType == Parameter.ParamType.MESSAGE_BODY && !method.isAnnotationPresent(Body.class))
                continue;
            if (builder.setter.paramType == Parameter.ParamType.UNKNOWN)
                continue;
            builder.buildSetter();
        }
    }

    protected void processMethod(boolean isLocator, ResourceClassBuilder resourceClassBuilder, Class<?> root,
            Method implementation) {
        Method method = getAnnotatedMethod(root, implementation);
        if (method != null) {
            Set<String> httpMethods = getHttpMethods(method);

            ResourceLocatorBuilder resourceLocatorBuilder;

            if (httpMethods == null) {
                resourceLocatorBuilder = resourceClassBuilder.locator(implementation, method);
            } else {
                ResourceMethodBuilder resourceMethodBuilder = resourceClassBuilder.method(implementation, method);
                resourceLocatorBuilder = resourceMethodBuilder;

                for (String httpMethod : httpMethods) {
                    if (httpMethod.equalsIgnoreCase(HttpMethod.GET))
                        resourceMethodBuilder.get();
                    else if (httpMethod.equalsIgnoreCase(HttpMethod.PUT))
                        resourceMethodBuilder.put();
                    else if (httpMethod.equalsIgnoreCase(HttpMethod.POST))
                        resourceMethodBuilder.post();
                    else if (httpMethod.equalsIgnoreCase(HttpMethod.DELETE))
                        resourceMethodBuilder.delete();
                    else if (httpMethod.equalsIgnoreCase(HttpMethod.OPTIONS))
                        resourceMethodBuilder.options();
                    else if (httpMethod.equalsIgnoreCase(HttpMethod.HEAD))
                        resourceMethodBuilder.head();
                    else
                        resourceMethodBuilder.httpMethod(httpMethod);
                }
                Produces produces = method.getAnnotation(Produces.class);
                if (produces == null)
                    produces = resourceClassBuilder.resourceClass.getClazz().getAnnotation(Produces.class);
                if (produces == null)
                    produces = method.getDeclaringClass().getAnnotation(Produces.class);
                if (produces != null)
                    resourceMethodBuilder.produces(produces.value());

                Consumes consumes = method.getAnnotation(Consumes.class);
                if (consumes == null)
                    consumes = resourceClassBuilder.resourceClass.getClazz().getAnnotation(Consumes.class);
                if (consumes == null)
                    consumes = method.getDeclaringClass().getAnnotation(Consumes.class);
                if (consumes != null)
                    resourceMethodBuilder.consumes(consumes.value());
            }
            Path methodPath = method.getAnnotation(Path.class);
            if (methodPath != null)
                resourceLocatorBuilder.path(methodPath.value());
            for (int i = 0; i < resourceLocatorBuilder.locator.params.length; i++) {
                resourceLocatorBuilder.param(i).fromAnnotations();
            }
            resourceLocatorBuilder.buildMethod();
        }
    }

    /**
     * Apply the list of {@link ResourceClassProcessor} to the supplied {@link ResourceClass}.
     */
    private ResourceClass applyProcessors(ResourceClass original) {
        ResourceClass current = original;
        for (List<ResourceClassProcessor> l : processors.values()) {
            for (ResourceClassProcessor processor : l) {
                current = processor.process(current);
                Objects.requireNonNull(current, "ResourceClassProcessor must not return null");
            }
        }
        return current;
    }

}
