package org.jboss.resteasy.core;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.GenericEntity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.WriterInterceptor;
import jakarta.ws.rs.sse.SseEventSink;

import org.jboss.resteasy.annotations.Stream;
import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext;
import org.jboss.resteasy.core.providerfactory.ResteasyProviderFactoryImpl;
import org.jboss.resteasy.core.registry.SegmentNode;
import org.jboss.resteasy.plugins.server.resourcefactory.JndiComponentResourceFactory;
import org.jboss.resteasy.plugins.server.resourcefactory.SingletonResource;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.specimpl.BuiltResponse;
import org.jboss.resteasy.specimpl.BuiltResponseEntityNotBacked;
import org.jboss.resteasy.specimpl.ResteasyUriInfo;
import org.jboss.resteasy.spi.ApplicationException;
import org.jboss.resteasy.spi.AsyncResponseProvider;
import org.jboss.resteasy.spi.AsyncStreamProvider;
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.InjectorFactory;
import org.jboss.resteasy.spi.MethodInjector;
import org.jboss.resteasy.spi.ResourceFactory;
import org.jboss.resteasy.spi.ResourceInvoker;
import org.jboss.resteasy.spi.ResteasyAsynchronousResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.UnhandledException;
import org.jboss.resteasy.spi.ValueInjector;
import org.jboss.resteasy.spi.interception.JaxrsInterceptorRegistry;
import org.jboss.resteasy.spi.interception.JaxrsInterceptorRegistryListener;
import org.jboss.resteasy.spi.metadata.MethodParameter;
import org.jboss.resteasy.spi.metadata.Parameter;
import org.jboss.resteasy.spi.metadata.ResourceMethod;
import org.jboss.resteasy.spi.statistics.MethodStatisticsLogger;
import org.jboss.resteasy.spi.validation.GeneralValidator;
import org.jboss.resteasy.spi.validation.GeneralValidatorCDI;
import org.jboss.resteasy.statistics.StatisticsControllerImpl;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.DynamicFeatureContextDelegate;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class ResourceMethodInvoker implements ResourceInvoker, JaxrsInterceptorRegistryListener {
    protected MethodInjector methodInjector;
    protected InjectorFactory injector;
    protected ResourceFactory resource;
    protected ResteasyProviderFactory parentProviderFactory;
    protected ResteasyProviderFactory resourceMethodProviderFactory;
    protected ResourceMethod method;
    protected Annotation[] methodAnnotations;
    protected ContainerRequestFilter[] requestFilters;
    protected ContainerResponseFilter[] responseFilters;
    protected WriterInterceptor[] writerInterceptors;
    protected ConcurrentHashMap<String, AtomicLong> stats = new ConcurrentHashMap<String, AtomicLong>();
    protected GeneralValidator validator;
    protected boolean isValidatable;
    protected boolean methodIsValidatable;
    @SuppressWarnings("rawtypes")
    protected AsyncResponseProvider asyncResponseProvider;
    @SuppressWarnings("rawtypes")
    AsyncStreamProvider asyncStreamProvider;
    protected boolean isSse;
    protected boolean isAsyncStreamProvider;
    protected ResourceInfo resourceInfo;

    protected boolean expectsBody;
    protected final boolean hasProduces;
    protected MethodStatisticsLogger methodStatisticsLogger;

    public ResourceMethodInvoker(final ResourceMethod method, final InjectorFactory injector, final ResourceFactory resource,
            final ResteasyProviderFactory providerFactory) {
        this.injector = injector;
        this.resource = resource;
        this.parentProviderFactory = providerFactory;
        this.method = method;
        this.methodAnnotations = this.method.getAnnotatedMethod().getAnnotations();
        methodStatisticsLogger = StatisticsControllerImpl.EMPTY;

        resourceInfo = new ResourceInfo() {
            @Override
            public Method getResourceMethod() {
                return ResourceMethodInvoker.this.method.getMethod();
            }

            @Override
            public Class<?> getResourceClass() {
                return ResourceMethodInvoker.this.method.getResourceClass().getClazz();
            }
        };

        Set<DynamicFeature> serverDynamicFeatures = providerFactory.getServerDynamicFeatures();
        if (serverDynamicFeatures != null && !serverDynamicFeatures.isEmpty()) {
            this.resourceMethodProviderFactory = new ResteasyProviderFactoryImpl(RuntimeType.SERVER, providerFactory);
            for (DynamicFeature feature : serverDynamicFeatures) {
                feature.configure(resourceInfo, new DynamicFeatureContextDelegate(resourceMethodProviderFactory));
            }
            ((ResteasyProviderFactoryImpl) this.resourceMethodProviderFactory).lockSnapshots();
        } else {
            // if no dynamic features, we don't need to copy the parent.
            this.resourceMethodProviderFactory = providerFactory;
        }

        this.methodInjector = injector.createMethodInjector(method, resourceMethodProviderFactory);

        // hack for when message contentType == null
        // and @Consumes is on the class
        expectsBody = this.methodInjector.expectsBody();

        requestFilters = resourceMethodProviderFactory.getContainerRequestFilterRegistry()
                .postMatch(method.getResourceClass().getClazz(), method.getAnnotatedMethod());
        responseFilters = resourceMethodProviderFactory.getContainerResponseFilterRegistry()
                .postMatch(method.getResourceClass().getClazz(), method.getAnnotatedMethod());
        writerInterceptors = resourceMethodProviderFactory.getServerWriterInterceptorRegistry()
                .postMatch(method.getResourceClass().getClazz(), method.getAnnotatedMethod());

        // register with parent to listen for redeploy events
        providerFactory.getContainerRequestFilterRegistry().getListeners().add(this);
        providerFactory.getContainerResponseFilterRegistry().getListeners().add(this);
        providerFactory.getServerWriterInterceptorRegistry().getListeners().add(this);
        ContextResolver<GeneralValidator> resolver = providerFactory.getContextResolver(GeneralValidator.class,
                MediaType.WILDCARD_TYPE);
        if (resolver != null) {
            validator = providerFactory.getContextResolver(GeneralValidator.class, MediaType.WILDCARD_TYPE).getContext(null);
        }
        if (validator != null) {
            Class<?> clazz = null;
            if (resource != null && resource.getScannableClass() != null) {
                clazz = resource.getScannableClass();
            } else {
                clazz = getMethod().getDeclaringClass();
            }
            if (resource instanceof JndiComponentResourceFactory) {
                isValidatable = true;
            } else {
                if (validator instanceof GeneralValidatorCDI) {
                    isValidatable = GeneralValidatorCDI.class.cast(validator).isValidatable(clazz, injector);
                } else {
                    isValidatable = validator.isValidatable(clazz);
                }
            }
            methodIsValidatable = validator.isMethodValidatable(getMethod());
        }

        asyncResponseProvider = resourceMethodProviderFactory.getAsyncResponseProvider(method.getReturnType());
        if (asyncResponseProvider == null) {
            asyncStreamProvider = resourceMethodProviderFactory.getAsyncStreamProvider(method.getReturnType());
        }
        if (asyncStreamProvider != null) {
            for (Annotation annotation : method.getAnnotatedMethod().getAnnotations()) {
                if (annotation.annotationType() == Stream.class) {
                    Stream stream = (Stream) annotation;
                    if (stream.value() == Stream.MODE.GENERAL) {
                        this.isAsyncStreamProvider = true;
                    }
                }
            }
        }

        if (isSseResourceMethod(method)) {
            isSse = true;
            method.markAsynchronous();
        }
        hasProduces = method.getMethod().isAnnotationPresent(Produces.class)
                || method.getMethod().getClass().isAnnotationPresent(Produces.class);
    }

    @Override
    public boolean hasProduces() {
        return hasProduces;
    }

    // spec section 9.3 Server API:
    // A resource method that injects an SseEventSink and
    // produces the media type text/event-stream is an SSE resource method.
    private boolean isSseResourceMethod(ResourceMethod resourceMethod) {

        // First exclusive condition to be a SSE resource method is to only
        // produce text/event-stream
        MediaType[] producedMediaTypes = resourceMethod.getProduces();
        boolean onlyProduceServerSentEventsMediaType = producedMediaTypes != null && producedMediaTypes.length == 1
                && MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(producedMediaTypes[0]);
        if (!onlyProduceServerSentEventsMediaType) {
            return false;
        }
        // Second condition to be a SSE resource method is to be injected with a
        // SseEventSink parameter
        MethodParameter[] resourceMethodParameters = resourceMethod.getParams();
        if (resourceMethodParameters != null) {
            for (MethodParameter resourceMethodParameter : resourceMethodParameters) {
                if (Parameter.ParamType.CONTEXT.equals(resourceMethodParameter.getParamType())
                        && SseEventSink.class.equals(resourceMethodParameter.getType())) {
                    return true;
                }
            }
        }

        // Resteasy specific:
        // Or the given application should register a
        // org.jboss.resteasy.spi.AsyncStreamProvider compatible with resource
        // method return type and the resource method must not be annotated with
        // any org.jboss.resteasy.annotations.Stream annotation
        if (asyncStreamProvider != null) {
            for (Annotation annotation : resourceMethod.getAnnotatedMethod().getAnnotations()) {
                if (annotation.annotationType() == Stream.class) {
                    return false;
                }
            }
            return true;
        }

        return false;
    }

    public void cleanup() {
        parentProviderFactory.getContainerRequestFilterRegistry().getListeners().remove(this);
        parentProviderFactory.getContainerResponseFilterRegistry().getListeners().remove(this);
        parentProviderFactory.getServerWriterInterceptorRegistry().getListeners().remove(this);
        for (ValueInjector param : methodInjector.getParams()) {
            if (param instanceof MessageBodyParameterInjector) {
                parentProviderFactory.getServerReaderInterceptorRegistry().getListeners().remove(param);
            }
        }
    }

    @Override
    public void registryUpdated(JaxrsInterceptorRegistry registry, JaxrsInterceptorRegistry.InterceptorFactory factory) {
        if (registry.getIntf().equals(WriterInterceptor.class)) {
            JaxrsInterceptorRegistry<WriterInterceptor> serverWriterInterceptorRegistry = this.resourceMethodProviderFactory
                    .getServerWriterInterceptorRegistry();
            //Check to prevent StackOverflowError
            if (registry != serverWriterInterceptorRegistry) {
                serverWriterInterceptorRegistry.register(factory);
            }
            this.writerInterceptors = serverWriterInterceptorRegistry.postMatch(this.method.getResourceClass().getClazz(),
                    this.method.getAnnotatedMethod());
        } else if (registry.getIntf().equals(ContainerRequestFilter.class)) {
            JaxrsInterceptorRegistry<ContainerRequestFilter> containerRequestFilterRegistry = this.resourceMethodProviderFactory
                    .getContainerRequestFilterRegistry();
            //Check to prevent StackOverflowError
            if (registry != containerRequestFilterRegistry) {
                containerRequestFilterRegistry.register(factory);
            }
            this.requestFilters = containerRequestFilterRegistry.postMatch(this.method.getResourceClass().getClazz(),
                    this.method.getAnnotatedMethod());
        } else if (registry.getIntf().equals(ContainerResponseFilter.class)) {
            JaxrsInterceptorRegistry<ContainerResponseFilter> containerResponseFilterRegistry = this.resourceMethodProviderFactory
                    .getContainerResponseFilterRegistry();
            //Check to prevent StackOverflowError
            if (registry != containerResponseFilterRegistry) {
                containerResponseFilterRegistry.register(factory);
            }
            this.responseFilters = containerResponseFilterRegistry.postMatch(this.method.getResourceClass().getClazz(),
                    this.method.getAnnotatedMethod());
        }
    }

    protected void incrementMethodCount(String httpMethod) {
        AtomicLong stat = stats.get(httpMethod);
        if (stat == null) {
            stat = new AtomicLong();
            AtomicLong old = stats.putIfAbsent(httpMethod, stat);
            if (old != null)
                stat = old;
        }
        stat.incrementAndGet();
    }

    /**
     * Key is httpMethod called.
     *
     * @return statistics map
     */
    public Map<String, AtomicLong> getStats() {
        return stats;
    }

    public ContainerRequestFilter[] getRequestFilters() {
        return requestFilters;
    }

    public ContainerResponseFilter[] getResponseFilters() {
        return responseFilters;
    }

    public WriterInterceptor[] getWriterInterceptors() {
        return writerInterceptors;
    }

    public Type getGenericReturnType() {
        return method.getGenericReturnType();
    }

    public Class<?> getResourceClass() {
        return method.getResourceClass().getClazz();
    }

    public Class<?> getReturnType() {
        return method.getReturnType();
    }

    public Annotation[] getMethodAnnotations() {
        return methodAnnotations;
    }

    @Override
    public Method getMethod() {
        return method.getMethod();
    }

    public CompletionStage<Object> invokeDryRun(HttpRequest request, HttpResponse response) {
        Object resource = this.resource.createResource(request, response, resourceMethodProviderFactory);
        if (resource instanceof CompletionStage) {
            @SuppressWarnings("unchecked")
            CompletionStage<Object> stage = (CompletionStage<Object>) resource;
            return stage
                    .thenCompose(target -> invokeDryRun(request, response, target));
        }
        return invokeDryRun(request, response, resource);
    }

    public BuiltResponse invoke(HttpRequest request, HttpResponse response) {
        Object resource = this.resource.createResource(request, response, resourceMethodProviderFactory);
        if (resource instanceof CompletionStage) {
            @SuppressWarnings("unchecked")
            CompletionStage<Object> stage = (CompletionStage<Object>) resource;
            return stage
                    .thenApply(target -> invoke(request, response, target)).toCompletableFuture().getNow(null);
        }
        return invoke(request, response, resource);
    }

    public CompletionStage<Object> invokeDryRun(HttpRequest request, HttpResponse response, Object target) {
        request.setAttribute(ResourceMethodInvoker.class.getName(), this);
        incrementMethodCount(request.getHttpMethod());
        ResteasyUriInfo uriInfo = (ResteasyUriInfo) request.getUri();
        if (method.getPath() != null) {
            uriInfo.pushMatchedURI(uriInfo.getMatchingPath());
        }
        uriInfo.pushCurrentResource(target);
        return invokeOnTargetDryRun(request, response, target);
    }

    public BuiltResponse invoke(HttpRequest request, HttpResponse response, Object target) {
        request.setAttribute(ResourceMethodInvoker.class.getName(), this);
        incrementMethodCount(request.getHttpMethod());
        ResteasyUriInfo uriInfo = (ResteasyUriInfo) request.getUri();
        if (method.getPath() != null) {
            uriInfo.pushMatchedURI(uriInfo.getMatchingPath());
        }
        uriInfo.pushCurrentResource(target);
        return invokeOnTarget(request, response, target);
    }

    @SuppressWarnings("unchecked")
    protected CompletionStage<Object> invokeOnTargetDryRun(HttpRequest request, HttpResponse response, Object target) {
        ResteasyContext.pushContext(ResourceInfo.class, resourceInfo); // we don't pop so writer interceptors can get at this
        ResteasyContext.pushContext(Configuration.class, resourceMethodProviderFactory);

        try {
            Object rtn = internalInvokeOnTarget(request, response, target);
            if (rtn != null && rtn instanceof CompletionStage) {
                return (CompletionStage<Object>) rtn;
            } else {
                return CompletableFuture.completedFuture(rtn);
            }
        } catch (Failure failure) {
            throw failure;
        } catch (ApplicationException appException) {
            throw appException;
        } catch (RuntimeException ex) {
            throw new ProcessingException(ex);

        }
    }

    protected BuiltResponse invokeOnTarget(HttpRequest request, HttpResponse response, Object target) {
        final RESTEasyTracingLogger tracingLogger = RESTEasyTracingLogger.getInstance(request);
        final long timestamp = tracingLogger.timestamp("METHOD_INVOKE");
        final long msTimeStamp = methodStatisticsLogger.timestamp();
        try {
            ResteasyContext.pushContext(ResourceInfo.class, resourceInfo); // we don't pop so writer interceptors can get at this
            ResteasyContext.pushContext(Configuration.class, resourceMethodProviderFactory);
            if (requestFilters != null && requestFilters.length > 0) {
                PostMatchContainerRequestContext requestContext = new PostMatchContainerRequestContext(request, this,
                        requestFilters,
                        () -> invokeOnTargetAfterFilter(request, response, target));
                // let it handle the continuation
                return requestContext.filter();
            } else {
                return invokeOnTargetAfterFilter(request, response, target);
            }
        } finally {
            methodStatisticsLogger.duration(msTimeStamp);
            if (resource instanceof SingletonResource) {
                tracingLogger.logDuration("METHOD_INVOKE", timestamp, ((SingletonResource) resource).traceInfo(),
                        method.getMethod());
            } else {
                tracingLogger.logDuration("METHOD_INVOKE", timestamp, resource, method.getMethod());
            }
        }
    }

    protected BuiltResponse invokeOnTargetAfterFilter(HttpRequest request, HttpResponse response, Object target) {
        if (validator != null) {
            if (isValidatable) {
                validator.validate(request, target);
            }
            if (methodIsValidatable) {
                request.setAttribute(GeneralValidator.class.getName(), validator);
            } else if (isValidatable) {
                validator.checkViolations(request);
            }
        }

        final AsyncResponseConsumer asyncResponseConsumer;
        if (asyncResponseProvider != null) {
            asyncResponseConsumer = AsyncResponseConsumer.makeAsyncResponseConsumer(this, asyncResponseProvider);
        } else if (asyncStreamProvider != null) {
            asyncResponseConsumer = AsyncResponseConsumer.makeAsyncResponseConsumer(this, asyncStreamProvider);
        } else {
            asyncResponseConsumer = null;
        }

        try {
            Object ret = internalInvokeOnTarget(request, response, target);
            if (ret != null && ret instanceof CompletionStage) {
                @SuppressWarnings("unchecked")
                CompletionStage<Object> retStage = (CompletionStage<Object>) ret;
                CompletionStage<BuiltResponse> stage = retStage
                        .thenApply(rtn -> afterInvoke(request, asyncResponseConsumer, rtn));
                // if async isn't finished, return null.  Container will assume that its a suspended request
                return stage.toCompletableFuture().getNow(null);
            } else {
                return afterInvoke(request, asyncResponseConsumer, CompletionStageHolder.resolve(ret));
            }
        } catch (CompletionException ex) {
            if (ex.getCause() instanceof RuntimeException)
                return handleInvocationException(asyncResponseConsumer, request, (RuntimeException) ex.getCause());
            SynchronousDispatcher.rethrow(ex.getCause());
            // never reached
            return null;
        } catch (RuntimeException ex) {
            return handleInvocationException(asyncResponseConsumer, request, ex);
        }
    }

    private BuiltResponse afterInvoke(HttpRequest request, AsyncResponseConsumer asyncResponseConsumer, Object rtn) {
        if (asyncResponseConsumer != null) {
            asyncResponseConsumer.subscribe(rtn);
            return null;
        }
        if (request.getAsyncContext().isSuspended()) {
            if (method.isAsynchronous())
                return null;
            // resume a sync request that got turned async by filters
            initializeAsync(request.getAsyncContext().getAsyncResponse());
            request.getAsyncContext().getAsyncResponse().resume(rtn);
            return null;
        }
        if (request.wasForwarded()) {
            return null;
        }
        if (!contextOutputStreamWrittenTo() && (rtn == null || method.getReturnType().equals(void.class))) {
            BuiltResponse build = (BuiltResponse) Response.noContent().build();
            build.addMethodAnnotations(getMethodAnnotations());
            return build;
        }
        if (Response.class.isAssignableFrom(method.getReturnType()) || rtn instanceof Response) {
            if (!(rtn instanceof BuiltResponse)) {
                Response r = (Response) rtn;
                Headers<Object> metadata = new Headers<Object>();
                metadata.putAll(r.getMetadata());
                rtn = new BuiltResponseEntityNotBacked(r.getStatus(), r.getStatusInfo().getReasonPhrase(),
                        metadata, r.getEntity(), null);
            }
            BuiltResponse rtn1 = (BuiltResponse) rtn;
            rtn1.addMethodAnnotations(getMethodAnnotations());
            if (rtn1.getGenericType() == null) {
                if (getMethod().getReturnType().equals(Response.class)) {
                    rtn1.setGenericType(rtn1.getEntityClass());
                } else {
                    rtn1.setGenericType(method.getGenericReturnType());
                }
            }
            return rtn1;
        }

        Response.ResponseBuilder builder = Response.ok(rtn);
        BuiltResponse jaxrsResponse = (BuiltResponse) builder.build();
        if (jaxrsResponse.getGenericType() == null) {
            if (getMethod().getReturnType().equals(Response.class)) {
                jaxrsResponse.setGenericType(jaxrsResponse.getEntityClass());
            } else {
                jaxrsResponse.setGenericType(method.getGenericReturnType());
            }
        }
        jaxrsResponse.addMethodAnnotations(getMethodAnnotations());
        return jaxrsResponse;
    }

    private BuiltResponse handleInvocationException(AsyncResponseConsumer asyncStreamResponseConsumer, HttpRequest request,
            RuntimeException ex) {
        if (asyncStreamResponseConsumer != null) {
            // WARNING: this can throw if the exception is not mapped by the user, in
            // which case we haven't completed the connection and called the callbacks
            try {
                AsyncResponseConsumer consumer = asyncStreamResponseConsumer;
                asyncStreamResponseConsumer.internalResume(ex, t -> consumer.complete(ex));
            } catch (UnhandledException x) {
                // make sure we call the callbacks before throwing to the container
                request.getAsyncContext().getAsyncResponse().completionCallbacks(ex);
                throw x;
            }
            return null;
        } else if (request.getAsyncContext().isSuspended()) {
            try {
                request.getAsyncContext().getAsyncResponse().resume(ex);
            } catch (Exception e) {
                LogMessages.LOGGER.errorResumingFailedAsynchOperation(e);
            }
            return null;
        } else {
            throw ex;
        }
    }

    @SuppressWarnings("unchecked")
    private Object internalInvokeOnTarget(HttpRequest request, HttpResponse response, Object target)
            throws Failure, ApplicationException {
        PostResourceMethodInvokers postResourceMethodInvokers = ResteasyContext
                .getContextData(PostResourceMethodInvokers.class);
        try {
            Object methodResponse = this.methodInjector.invoke(request, response, target);
            CompletionStage<Object> stage = null;
            if (methodResponse != null && methodResponse instanceof CompletionStage) {
                stage = (CompletionStage<Object>) methodResponse;
                return stage
                        .handle((ret, exception) -> {
                            // on success
                            if (exception == null && postResourceMethodInvokers != null) {
                                postResourceMethodInvokers.getInvokers().forEach(e -> e.invoke());
                            }
                            // finally
                            if (postResourceMethodInvokers != null) {
                                postResourceMethodInvokers.clear();
                            }
                            if (exception != null) {
                                SynchronousDispatcher.rethrow(exception);
                                // never reached
                                return null;
                            }
                            return ret;
                        });
            } else {
                // on success
                if (postResourceMethodInvokers != null) {
                    postResourceMethodInvokers.getInvokers().forEach(e -> e.invoke());
                }
                // finally
                if (postResourceMethodInvokers != null) {
                    postResourceMethodInvokers.clear();
                }
                return methodResponse;
            }
        } catch (RuntimeException failure) {
            if (postResourceMethodInvokers != null) {
                postResourceMethodInvokers.clear();
            }
            throw failure;
        }
    }

    public void initializeAsync(ResteasyAsynchronousResponse asyncResponse) {
        asyncResponse.setAnnotations(method.getAnnotatedMethod().getAnnotations());
        asyncResponse.setWriterInterceptors(getWriterInterceptors());
        asyncResponse.setResponseFilters(getResponseFilters());
        if (asyncResponse instanceof ResourceMethodInvokerAwareResponse) {
            ((ResourceMethodInvokerAwareResponse) asyncResponse).setMethod(this);
        }
    }

    public boolean doesProduce(List<? extends MediaType> accepts) {
        if (accepts == null || accepts.size() == 0) {
            //System.out.println("**** no accepts " +" method: " + method);
            return true;
        }
        if (method.getProduces().length == 0) {
            //System.out.println("**** no produces " +" method: " + method);
            return true;
        }

        for (MediaType accept : accepts) {
            for (MediaType type : method.getProduces()) {
                if (type.isCompatible(accept)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean doesConsume(MediaType contentType) {
        boolean matches = false;
        if (method.getConsumes().length == 0 || (contentType == null && !expectsBody))
            return true;

        if (contentType == null) {
            contentType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
        }
        for (MediaType type : method.getConsumes()) {
            if (type.isCompatible(contentType)) {
                matches = true;
                break;
            }
        }
        return matches;
    }

    public MediaType resolveContentType(HttpRequest in, Object entity) {
        MediaType chosen = (MediaType) in.getAttribute(SegmentNode.RESTEASY_CHOSEN_ACCEPT);
        if (chosen != null && !chosen.equals(MediaType.WILDCARD_TYPE)) {
            return chosen;
        }

        List<MediaType> accepts = in.getHttpHeaders().getAcceptableMediaTypes();

        if (accepts == null || accepts.size() == 0) {
            if (method.getProduces().length == 0)
                return MediaType.WILDCARD_TYPE;
            else
                return method.getProduces()[0];
        }

        if (method.getProduces().length == 0) {
            return resolveContentTypeByAccept(accepts, entity);
        }

        for (MediaType accept : accepts) {
            for (MediaType type : method.getProduces()) {
                if (type.isCompatible(accept))
                    return type;
            }
        }
        return MediaType.WILDCARD_TYPE;
    }

    protected MediaType resolveContentTypeByAccept(List<MediaType> accepts, Object entity) {
        if (accepts == null || accepts.size() == 0 || entity == null) {
            return MediaType.WILDCARD_TYPE;
        }
        Class<?> clazz = entity.getClass();
        Type type = this.method.getGenericReturnType();
        if (entity instanceof GenericEntity) {
            GenericEntity<?> gen = (GenericEntity<?>) entity;
            clazz = gen.getRawType();
            type = gen.getType();
        }
        for (MediaType accept : accepts) {
            if (resourceMethodProviderFactory.getMessageBodyWriter(clazz, type, method.getAnnotatedMethod().getAnnotations(),
                    accept) != null) {
                return accept;
            }
        }
        return MediaType.WILDCARD_TYPE;
    }

    /**
     * Checks if any bytes were written to a @Context HttpServletResponse
     *
     * @see ContextParameterInjector for details
     *      Fix for RESTEASY-1721
     */
    private boolean contextOutputStreamWrittenTo() {
        for (ValueInjector vi : methodInjector.getParams()) {
            if (vi instanceof ContextParameterInjector) {
                return ((ContextParameterInjector) vi).isOutputStreamWasWritten();
            }
        }
        return false;
    }

    public Set<String> getHttpMethods() {
        return method.getHttpMethods();
    }

    public MediaType[] getProduces() {
        return method.getProduces();
    }

    public MediaType[] getConsumes() {
        return method.getConsumes();
    }

    public boolean isSse() {
        return isSse;
    }

    public boolean isAsyncStreamProvider() {
        return isAsyncStreamProvider;
    }

    public void markMethodAsAsync() {
        method.markAsynchronous();
    }

    public void setMethodStatisticsLogger(MethodStatisticsLogger msLogger) {
        methodStatisticsLogger = msLogger;
    }

    public MethodStatisticsLogger getMethodStatisticsLogger() {
        return methodStatisticsLogger;
    }
}
