package org.jboss.resteasy.core.providerfactory;

import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.core.EntityPart;
import org.jboss.resteasy.core.InjectorFactoryImpl;
import org.jboss.resteasy.core.MediaTypeMap;
import org.jboss.resteasy.core.ResteasyContext;
import org.jboss.resteasy.core.se.ResteasySeConfiguration;
import org.jboss.resteasy.core.se.ResteasySeInstance;
import org.jboss.resteasy.plugins.delegates.CacheControlDelegate;
import org.jboss.resteasy.plugins.delegates.CookieHeaderDelegate;
import org.jboss.resteasy.plugins.delegates.DateDelegate;
import org.jboss.resteasy.plugins.delegates.EntityTagDelegate;
import org.jboss.resteasy.plugins.delegates.LinkDelegate;
import org.jboss.resteasy.plugins.delegates.LinkHeaderDelegate;
import org.jboss.resteasy.plugins.delegates.LocaleDelegate;
import org.jboss.resteasy.plugins.delegates.MediaTypeHeaderDelegate;
import org.jboss.resteasy.plugins.delegates.NewCookieHeaderDelegate;
import org.jboss.resteasy.plugins.delegates.UriHeaderDelegate;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.AsyncClientResponseProvider;
import org.jboss.resteasy.spi.AsyncResponseProvider;
import org.jboss.resteasy.spi.AsyncStreamProvider;
import org.jboss.resteasy.spi.ConstructorInjector;
import org.jboss.resteasy.spi.ContextInjector;
import org.jboss.resteasy.spi.Dispatcher;
import org.jboss.resteasy.spi.HeaderValueProcessor;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.InjectorFactory;
import org.jboss.resteasy.spi.LinkHeader;
import org.jboss.resteasy.spi.PropertyInjector;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.StringParameterUnmarshaller;
import org.jboss.resteasy.spi.concurrent.ThreadContext;
import org.jboss.resteasy.spi.concurrent.ThreadContexts;
import org.jboss.resteasy.spi.interception.JaxrsInterceptorRegistry;
import org.jboss.resteasy.spi.metadata.ResourceBuilder;
import org.jboss.resteasy.spi.metadata.ResourceClassProcessor;
import org.jboss.resteasy.spi.statistics.StatisticsController;
import org.jboss.resteasy.spi.util.PickConstructor;
import org.jboss.resteasy.spi.util.Types;
import org.jboss.resteasy.statistics.StatisticsControllerImpl;
import org.jboss.resteasy.tracing.RESTEasyTracingLogger;
import org.jboss.resteasy.util.FeatureContextDelegate;
import org.jboss.resteasy.util.snapshot.SnapshotMap;
import org.jboss.resteasy.util.snapshot.SnapshotSet;

import jakarta.ws.rs.ConstrainedTo;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.client.ClientResponseFilter;
import jakarta.ws.rs.client.RxInvoker;
import jakarta.ws.rs.client.RxInvokerProvider;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.CacheControl;
import jakarta.ws.rs.core.Configurable;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.Variant;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import jakarta.ws.rs.ext.Providers;
import jakarta.ws.rs.ext.ReaderInterceptor;
import jakarta.ws.rs.ext.RuntimeDelegate;
import jakarta.ws.rs.ext.WriterInterceptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class ResteasyProviderFactoryImpl extends ResteasyProviderFactory implements Providers, HeaderValueProcessor, Configurable<ResteasyProviderFactory>, Configuration
{
   protected ClientHelper clientHelper;
   protected ServerHelper serverHelper;

   protected SnapshotSet<Class<?>> providerClasses;
   protected SnapshotSet<Object> providerInstances;
   protected SnapshotMap<Class<?>, Map<Class<?>, Integer>> classContracts;
   protected SnapshotMap<String, Object> properties;

   protected SnapshotMap<Class<?>, HeaderDelegate> headerDelegates;
   protected SnapshotMap<Type, ContextInjector> contextInjectors;
   protected SnapshotMap<Type, ContextInjector> asyncContextInjectors;
   protected SnapshotMap<Class<?>, Class<? extends StringParameterUnmarshaller>> stringParameterUnmarshallers;
   protected SnapshotSet<Feature> enabledFeatures;

   protected boolean attachedContextResolvers;
   protected Map<Class<?>, MediaTypeMap<SortedKey<ContextResolver>>> contextResolvers;
   protected boolean attachedParamConverterProviders;
   protected Set<ExtSortedKey<ParamConverterProvider>> sortedParamConverterProviders;

   protected Set<Class<?>> alreadyEstablishedNullHeaderDelegate = ConcurrentHashMap.newKeySet();
   protected boolean builtinsRegistered = false;
   protected boolean registerBuiltins = true;
   protected InjectorFactory injectorFactory;
   protected ResourceBuilder resourceBuilder;
   protected boolean initialized = false;
   protected boolean lockSnapshots;
   protected StatisticsControllerImpl statisticsController = new StatisticsControllerImpl();

   public ResteasyProviderFactoryImpl()
   {
      // NOTE!!! It is important to put all initialization into initialize() as ThreadLocalResteasyProviderFactory
      // subclasses and delegates to this class.
      initialize();
   }

   /**
    * Create factory optimized for a Client
    *
    * @param runtimeType
    */
   public ResteasyProviderFactoryImpl(final RuntimeType runtimeType)
   {
      if (runtimeType != RuntimeType.CLIENT) throw new IllegalStateException();
      this.clientHelper = new ClientHelper(this);
      this.serverHelper = NOOPServerHelper.SINGLETON;
      initializeCommon(null, true, false);
      // don't know when client will be made shareable so just do it here
      lockSnapshots();
   }

   /**
    *
    * @param runtimeType
    */
   /**
    * Create factory optimized for a specific RuntimeType that is a copy of its parent (shallow copy if possible)
    *
    * @param runtimeType
    * @param parent
    */
   public ResteasyProviderFactoryImpl(final RuntimeType runtimeType, final ResteasyProviderFactory parent)
   {
      if (runtimeType == RuntimeType.CLIENT) {
         ResteasyProviderFactoryImpl impl = (ResteasyProviderFactoryImpl) parent;
         this.clientHelper = new ClientHelper(this, impl.clientHelper);
         this.serverHelper = NOOPServerHelper.SINGLETON;
         this.lockSnapshots = true;
         initializeCommon(impl, true, false);
         // don't know when client will be made shareable so just do it here
         lockSnapshots();
      } else {
         ResteasyProviderFactoryImpl parentImpl = (ResteasyProviderFactoryImpl)parent;
         clientHelper = NOOPClientHelper.SINGLETON;
         serverHelper = new ServerHelper(this, parentImpl.serverHelper);
         initializeCommon(parentImpl, false, true);
      }
   }

   protected void registerBuiltin()
   {
      RegisterBuiltin.register(this);
   }

   protected void initializeCommon(ResteasyProviderFactoryImpl parent, boolean lockSnapshots, boolean snapFirst)
   {
      properties = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.properties, true, lockSnapshots, snapFirst);

      providerClasses = parent == null ? new SnapshotSet<>(lockSnapshots) : new SnapshotSet<>(parent.providerClasses, true, lockSnapshots, snapFirst);
      providerInstances = parent == null ? new SnapshotSet<>(lockSnapshots) : new SnapshotSet<>(parent.providerInstances, true, lockSnapshots, snapFirst);
      classContracts = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.classContracts, true, lockSnapshots, snapFirst);

      enabledFeatures = parent == null ? new SnapshotSet<>(lockSnapshots) : new SnapshotSet<>(parent.enabledFeatures, true, lockSnapshots, snapFirst);
      if (parent != null) {
         if (snapFirst) {
            // resourcemethod invoker factory
            // we don't want to copy these
            attachedParamConverterProviders = true;
            sortedParamConverterProviders = parent.sortedParamConverterProviders;
            attachedContextResolvers = true;
            contextResolvers = parent.contextResolvers;
         } else {
            contextResolvers = new ConcurrentHashMap<>();
            for (Entry<Class<?>, MediaTypeMap<SortedKey<ContextResolver>>> entry : parent.contextResolvers.entrySet())
            {
               contextResolvers.put(entry.getKey(), new MediaTypeMap<>(entry.getValue()));
            }
            sortedParamConverterProviders = Collections.synchronizedSortedSet(new TreeSet<>(parent.sortedParamConverterProviders));
         }
      } else {
         contextResolvers = new ConcurrentHashMap<>();
         sortedParamConverterProviders = Collections.synchronizedSortedSet(new TreeSet<>());
      }

      resourceBuilder = new ResourceBuilder();
      headerDelegates = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.getHeaderDelegates(), true, lockSnapshots, snapFirst);
      if (parent == null) {
         // parent should always have these delegates
         addHeaderDelegateIfAbsent(MediaType.class, MediaTypeHeaderDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(NewCookie.class, NewCookieHeaderDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(Cookie.class, CookieHeaderDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(URI.class, UriHeaderDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(EntityTag.class, EntityTagDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(CacheControl.class, CacheControlDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(Locale.class, LocaleDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(LinkHeader.class, LinkHeaderDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(jakarta.ws.rs.core.Link.class, LinkDelegate.INSTANCE);
         addHeaderDelegateIfAbsent(Date.class, DateDelegate.INSTANCE);
      }

      builtinsRegistered = false;
      registerBuiltins = true;

      stringParameterUnmarshallers = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.stringParameterUnmarshallers, true, lockSnapshots, snapFirst);
      contextInjectors = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.contextInjectors, true, lockSnapshots, snapFirst);
      asyncContextInjectors = parent == null ? new SnapshotMap<>(lockSnapshots) : new SnapshotMap<>(parent.asyncContextInjectors, true, lockSnapshots, snapFirst);


      injectorFactory = parent == null ? InjectorFactoryImpl.INSTANCE : parent.getInjectorFactory();
      initialized = true;
   }

   private void copyResolversIfNeeded() {
      if (!attachedContextResolvers) return;
      Map<Class<?>, MediaTypeMap<SortedKey<ContextResolver>>> newResolvers = new ConcurrentHashMap<>();
      for (Entry<Class<?>, MediaTypeMap<SortedKey<ContextResolver>>> entry : contextResolvers.entrySet())
      {
         newResolvers.put(entry.getKey(), new MediaTypeMap<>(entry.getValue()));
      }
      contextResolvers = newResolvers;
      attachedContextResolvers = false;
   }

   protected void initialize()
   {
      initializeCommon(null, false, false);
      clientHelper = new ClientHelper(this);
      serverHelper = new ServerHelper(this);
   }

   public void lockSnapshots() {
      lockSnapshots = true;
      if (providerClasses != null) providerClasses.lockSnapshots();
      if (providerInstances != null) providerInstances.lockSnapshots();
      if (classContracts != null) classContracts.lockSnapshots();
      if (properties != null) properties.lockSnapshots();
      if (headerDelegates != null) headerDelegates.lockSnapshots();
      if (contextInjectors != null) contextInjectors.lockSnapshots();
      if (asyncContextInjectors != null) asyncContextInjectors.lockSnapshots();
      if (stringParameterUnmarshallers != null) stringParameterUnmarshallers.lockSnapshots();
      if (enabledFeatures != null) enabledFeatures.lockSnapshots();
      clientHelper.lockSnapshots();
      serverHelper.lockSnapshots();
   }

   public Set<DynamicFeature> getServerDynamicFeatures()
   {
      return serverHelper.getDynamicFeatures();
   }

   public Set<DynamicFeature> getClientDynamicFeatures()
   {
      return clientHelper.getDynamicFeatures();
   }

   protected MediaTypeMap<SortedKey<MessageBodyReader>> getServerMessageBodyReaders()
   {
      return serverHelper.getMessageBodyReaders();
   }

   protected MediaTypeMap<SortedKey<MessageBodyWriter>> getServerMessageBodyWriters()
   {
      return serverHelper.getMessageBodyWriters();
   }

   protected MediaTypeMap<SortedKey<MessageBodyReader>> getClientMessageBodyReaders()
   {
      return clientHelper.getMessageBodyReaders();
   }

   protected MediaTypeMap<SortedKey<MessageBodyWriter>> getClientMessageBodyWriters()
   {
      return clientHelper.getMessageBodyWriters();
   }

   private Map<Class<?>, SortedKey<ExceptionMapper>> getSortedExceptionMappers()
   {
      return serverHelper.getExceptionMappers();
   }

   public Map<Class<?>, AsyncResponseProvider> getAsyncResponseProviders()
   {
      return serverHelper.getAsyncResponseProviders();
   }

   public Map<Class<?>, AsyncStreamProvider> getAsyncStreamProviders()
   {
      return serverHelper.getAsyncStreamProviders();
   }

   public Map<Class<?>, AsyncClientResponseProvider> getAsyncClientResponseProviders()
   {
      return clientHelper.getAsyncClientResponseProviders();
   }

   public Map<Type, ContextInjector> getContextInjectors()
   {
      return contextInjectors;
   }

   public Map<Type, ContextInjector> getAsyncContextInjectors()
   {
      return asyncContextInjectors;
   }

   protected Map<Class<?>, MediaTypeMap<SortedKey<ContextResolver>>> getContextResolvers()
   {
      return contextResolvers;
   }

   protected Set<ExtSortedKey<ParamConverterProvider>> getSortedParamConverterProviders()
   {
      return sortedParamConverterProviders;
   }

   protected Map<Class<?>, Class<? extends StringParameterUnmarshaller>> getStringParameterUnmarshallers()
   {
      return stringParameterUnmarshallers;
   }

   /**
    * Gets provide classes.
    *
    * @return set of provider classes
    */
   public Set<Class<?>> getProviderClasses()
   {
      return Collections.unmodifiableSet(providerClasses);
   }

   /**
    * Gets provider instances.
    *
    * @return set of provider instances
    */
   public Set<Object> getProviderInstances()
   {
      return Collections.unmodifiableSet(providerInstances);
   }

   public Map<Class<?>, Map<Class<?>, Integer>> getClassContracts()
   {
      return classContracts;
   }

   public <T> T getContextData(Class<T> rawType, Type genericType, Annotation[] annotations, boolean unwrapAsync)
   {
      T ret = (T) ResteasyContext.getContextDataMap().get(rawType);
      if (ret != null)
         return ret;
      ContextInjector contextInjector = getContextInjectors().get(genericType);
      boolean async = false;
      if (contextInjector == null && unwrapAsync)
      {
         contextInjector = getAsyncContextInjectors().get(Types.boxPrimitives(genericType));
         async = true;
      }

      if (contextInjector != null)
      {
         ret = (T) contextInjector.resolve(rawType, genericType, annotations);
         if (async && ret != null)
         {
            Type wrappedType = Types.getActualTypeArgumentsOfAnInterface(contextInjector.getClass(),
                  ContextInjector.class)[0];
            Class<?> rawWrappedType = Types.getRawType(wrappedType);
            AsyncResponseProvider converter = getAsyncResponseProvider(rawWrappedType);
            // OK this is plain lying
            ret = (T) converter.toCompletionStage(ret);
         }
      }
      return ret;
   }

   public boolean isRegisterBuiltins()
   {
      return registerBuiltins;
   }

   public void setRegisterBuiltins(boolean registerBuiltins)
   {
      this.registerBuiltins = registerBuiltins;
   }

   public InjectorFactory getInjectorFactory()
   {
      return injectorFactory;
   }

   public void setInjectorFactory(InjectorFactory injectorFactory)
   {
      this.injectorFactory = injectorFactory;
   }

   public JaxrsInterceptorRegistry<ReaderInterceptor> getServerReaderInterceptorRegistry()
   {
      return serverHelper.getReaderInterceptorRegistry();
   }

   public JaxrsInterceptorRegistry<WriterInterceptor> getServerWriterInterceptorRegistry()
   {
      return serverHelper.getWriterInterceptorRegistry();
   }

   public JaxrsInterceptorRegistry<ContainerRequestFilter> getContainerRequestFilterRegistry()
   {
      return serverHelper.getRequestFilters();
   }

   public JaxrsInterceptorRegistry<ContainerResponseFilter> getContainerResponseFilterRegistry()
   {
      return serverHelper.getResponseFilters();
   }

   public JaxrsInterceptorRegistry<ReaderInterceptor> getClientReaderInterceptorRegistry()
   {
      return clientHelper.getReaderInterceptorRegistry();
   }

   public JaxrsInterceptorRegistry<WriterInterceptor> getClientWriterInterceptorRegistry()
   {
      return clientHelper.getWriterInterceptorRegistry();
   }

   public JaxrsInterceptorRegistry<ClientRequestFilter> getClientRequestFilterRegistry()
   {
      return clientHelper.getRequestFilters();
   }

   public JaxrsInterceptorRegistry<ClientResponseFilter> getClientResponseFilters()
   {
      return clientHelper.getResponseFilters();
   }

   public boolean isBuiltinsRegistered()
   {
      return builtinsRegistered;
   }

   public void setBuiltinsRegistered(boolean builtinsRegistered)
   {
      this.builtinsRegistered = builtinsRegistered;
   }

   public UriBuilder createUriBuilder()
   {
      return Utils.createUriBuilder();
   }

   public Response.ResponseBuilder createResponseBuilder()
   {
      return Utils.createResponseBuilder();
   }

   public Variant.VariantListBuilder createVariantListBuilder()
   {
      return Utils.createVariantListBuilder();
   }

   public <T> HeaderDelegate<T> createHeaderDelegate(Class<T> tClass)
   {
      if (tClass == null)
         throw new IllegalArgumentException(Messages.MESSAGES.tClassParameterNull());
      return Utils.createHeaderDelegate(headerDelegates, alreadyEstablishedNullHeaderDelegate, tClass);
   }

   protected Map<Class<?>, HeaderDelegate> getHeaderDelegates()
   {
      return headerDelegates;
   }

   public void addHeaderDelegate(Class clazz, HeaderDelegate header)
   {
       headerDelegates.put(clazz, header);
   }

   protected void addHeaderDelegateIfAbsent(Class clazz, HeaderDelegate header)
   {
      if (!headerDelegates.containsKey(clazz))
      {
         addHeaderDelegate(clazz, header);
      }
   }

   @Deprecated
   public <T> MessageBodyReader<T> getServerMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders = getServerMessageBodyReaders();
      return resolveMessageBodyReader(type, genericType, annotations, mediaType, availableReaders);
   }

   public <T> MessageBodyReader<T> getServerMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType, RESTEasyTracingLogger tracingLogger)
   {
      MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders = getServerMessageBodyReaders();
      return resolveMessageBodyReader(type, genericType, annotations, mediaType, availableReaders, tracingLogger);
   }

   /**
    * Always returns server MBRs.
    *
    * @param type        the class of the object that is to be read.
    * @param genericType the type of object to be produced. E.g. if the
    *                    message body is to be converted into a method parameter, this will be
    *                    the formal type of the method parameter as returned by
    *                    {@code Class.getGenericParameterTypes}.
    * @param annotations an array of the annotations on the declaration of the
    *                    artifact that will be initialized with the produced instance. E.g. if
    *                    the message body is to be converted into a method parameter, this will
    *                    be the annotations on that parameter returned by
    *                    {@code Class.getParameterAnnotations}.
    * @param mediaType   the media type of the data that will be read.
    * @param <T> type
    * @return message reader
    */
   public <T> MessageBodyReader<T> getMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders = getServerMessageBodyReaders();
      MessageBodyReader<T> reader = resolveMessageBodyReader(type, genericType, annotations, mediaType,
            availableReaders);
      if (reader != null)
         LogMessages.LOGGER.debugf("MessageBodyReader: %s", reader.getClass().getName());
      return reader;
   }

   public <T> MessageBodyReader<T> getClientMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders = getClientMessageBodyReaders();
      if (availableReaders == null) return null;
      return resolveMessageBodyReader(type, genericType, annotations, mediaType, availableReaders);
   }

   @Deprecated
   private <T> MessageBodyReader<T> resolveMessageBodyReader(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType, MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders)
   {
      if (availableReaders == null) return null;
      List<SortedKey<MessageBodyReader>> readers = availableReaders.getPossible(mediaType, type);

      //logger.info("******** getMessageBodyReader *******");
      for (SortedKey<MessageBodyReader> reader : readers)
      {
         //logger.info("     matching reader: " + reader.getClass().getName());
         if (reader.getObj().isReadable(type, genericType, annotations, mediaType))
         {
            LogMessages.LOGGER.debugf("MessageBodyReader: %s", reader.getClass().getName());
            return (MessageBodyReader<T>) reader.getObj();
         }
      }
      return null;
   }

   private <T> MessageBodyReader<T> resolveMessageBodyReader(Class<T> type, Type genericType,
         Annotation[] annotations, MediaType mediaType, MediaTypeMap<SortedKey<MessageBodyReader>> availableReaders,
         RESTEasyTracingLogger tracingLogger)
   {
      if (availableReaders == null) return null;
      List<SortedKey<MessageBodyReader>> readers = availableReaders.getPossible(mediaType, type);

      if (tracingLogger.isLogEnabled("MBR_FIND"))
      {
         tracingLogger.log("MBR_FIND", type.getName(),
               (genericType instanceof Class ? ((Class) genericType).getName() : genericType), mediaType,
               java.util.Arrays.toString(annotations));
      }

      MessageBodyReader<T> result = null;

      Iterator<SortedKey<MessageBodyReader>> iterator = readers.iterator();

      while (iterator.hasNext())
      {
         final SortedKey<MessageBodyReader> reader = iterator.next();

         if (reader.getObj().isReadable(type, genericType, annotations, mediaType))
         {
            LogMessages.LOGGER.debugf("MessageBodyReader: %s", reader.getClass().getName());
            result = (MessageBodyReader<T>) reader.getObj();
            tracingLogger.log("MBR_SELECTED", reader);
            break;
         }
         tracingLogger.log("MBR_NOT_READABLE", result);
      }

      if (tracingLogger.isLogEnabled("MBR_SKIPPED"))
      {
         while (iterator.hasNext())
         {
            final SortedKey<MessageBodyReader> reader = iterator.next();
            tracingLogger.log("MBR_SKIPPED", reader.getObj());
         }
      }
      return result;
   }

   private void addContextInjector(ContextInjector provider, Class providerClass)
   {
      Type[] typeArgs = Types.getActualTypeArgumentsOfAnInterface(providerClass, ContextInjector.class);
      Utils.injectProperties(this, provider.getClass(), provider);

      contextInjectors.put(typeArgs[0], provider);

      if (!Objects.equals(typeArgs[0], typeArgs[1]))
      {
         asyncContextInjectors.put(typeArgs[1], provider);
      }
   }

   private void addContextResolver(ContextResolver provider, int priority, Class providerClass, boolean builtin)
   {
      // RESTEASY-1725
      if (providerClass.getName().contains("$$Lambda$"))
      {
         throw new RuntimeException(Messages.MESSAGES.registeringContextResolverAsLambda());
      }
      copyResolversIfNeeded();
      Type typeParameter = Types.getActualTypeArgumentsOfAnInterface(providerClass, ContextResolver.class)[0];
      Utils.injectProperties(this, providerClass, provider);
      Class<?> parameterClass = Types.getRawType(typeParameter);
      MediaTypeMap<SortedKey<ContextResolver>> resolvers = contextResolvers.get(parameterClass);
      if (resolvers == null)
      {
         resolvers = new MediaTypeMap<SortedKey<ContextResolver>>();
         contextResolvers.put(parameterClass, resolvers);
      }
      Produces produces = provider.getClass().getAnnotation(Produces.class);
      SortedKey<ContextResolver> key = new SortedKey<ContextResolver>(ContextResolver.class, provider, providerClass, priority, builtin);
      if (produces != null)
      {
         for (String produce : produces.value())
         {
            resolvers.add(produce, key);
         }
      }
      else
      {
         resolvers.add(MediaType.WILDCARD, key);
      }
   }

   public void addStringParameterUnmarshaller(Class<? extends StringParameterUnmarshaller> provider)
   {
      Type[] intfs = provider.getGenericInterfaces();
      for (Type type : intfs)
      {
         if (type instanceof ParameterizedType)
         {
            ParameterizedType pt = (ParameterizedType) type;
            if (pt.getRawType().equals(StringParameterUnmarshaller.class))
            {
               Class<?> aClass = Types.getRawType(pt.getActualTypeArguments()[0]);
               stringParameterUnmarshallers.put(aClass, provider);
            }
         }
      }
   }

   public List<ContextResolver> getContextResolvers(final Class<?> clazz, MediaType type)
   {
      if (getContextResolvers() == null) return null;
      MediaTypeMap<SortedKey<ContextResolver>> resolvers = getContextResolvers().get(clazz);
      if (resolvers == null)
         return null;
      List<ContextResolver> rtn = new ArrayList<ContextResolver>();
      List<SortedKey<ContextResolver>> list = resolvers.getPossible(type);
      list.forEach(resolver -> rtn.add(resolver.getObj()));
      return rtn;
   }

   public ParamConverter getParamConverter(Class clazz, Type genericType, Annotation[] annotations)
   {
      try
      {
         ResteasyContext.pushContext(ResteasyProviderFactory.class, this); // For MultiValuedParamConverterProvider
         for (SortedKey<ParamConverterProvider> provider : getSortedParamConverterProviders())
         {
            ParamConverter converter = provider.getObj().getConverter(clazz, genericType, annotations);
            if (converter != null) return converter;
         }
         return null;
      }
      finally
      {
         ResteasyContext.popContextData(ResteasyProviderFactory.class);
      }
   }

   public <T> StringParameterUnmarshaller<T> createStringParameterUnmarshaller(Class<T> clazz)
   {
      if (getStringParameterUnmarshallers() == null || getStringParameterUnmarshallers().isEmpty())
         return null;
      Class<? extends StringParameterUnmarshaller> un = getStringParameterUnmarshallers().get(clazz);
      if (un == null)
         return null;
      StringParameterUnmarshaller<T> provider = injectedInstance(un);
      return provider;

   }

   public void registerProvider(Class provider)
   {
      registerProvider(provider, false);
   }

   /**
    * Convert an object to a string.  First try StringConverter then, object.ToString()
    *
    * @param object object
    * @param clazz class
    * @param genericType generic type
    * @param annotations array of annotation
    * @return string representation
    */
   public String toString(Object object, Class clazz, Type genericType, Annotation[] annotations)
   {
      if (object instanceof String)
         return (String) object;
      ParamConverter paramConverter = getParamConverter(clazz, genericType, annotations);
      if (paramConverter != null)
      {
         return paramConverter.toString(object);
      }
      return object.toString();
   }

   @Override
   public String toHeaderString(Object object)
   {
      if (object == null)
         return "";
      if (object instanceof String)
         return (String) object;
      Class<?> aClass = object.getClass();
      ParamConverter paramConverter = getParamConverter(aClass, null, null);
      if (paramConverter != null)
      {
         return paramConverter.toString(object);
      }
      HeaderDelegate delegate = getHeaderDelegate(aClass);
      if (delegate != null)
         return delegate.toString(object);
      else
         return object.toString();

   }

   /**
    * Checks to see if RuntimeDelegate is a ResteasyProviderFactory
    * If it is, then use that, otherwise use this.
    *
    * @param aClass class of the header
    * @return header delegate
    */
   public HeaderDelegate getHeaderDelegate(Class<?> aClass)
   {
      HeaderDelegate delegate = null;
      // Stupid idiotic TCK calls RuntimeDelegate.setInstance()
      if (RuntimeDelegate.getInstance() instanceof ResteasyProviderFactory)
      {
         delegate = createHeaderDelegate(aClass);
      }
      else
      {
         delegate = RuntimeDelegate.getInstance().createHeaderDelegate(aClass);
      }
      return delegate;
   }

   /**
    * Register a @Provider class.  Can be a MessageBodyReader/Writer or ExceptionMapper.
    *
    * @param provider provider class
    * @param isBuiltin built-in
    */
   public void registerProvider(Class provider, boolean isBuiltin)
   {
      registerProvider(provider, null, isBuiltin, null);
   }

   public void registerProvider(Class provider, Integer priorityOverride, boolean isBuiltin,
         Map<Class<?>, Integer> contracts)
   {
      Map<Class<?>, Map<Class<?>, Integer>> classContracts = getClassContracts();
      if (classContracts.containsKey(provider))
      {
         LogMessages.LOGGER.providerClassAlreadyRegistered(provider.getName());
         return;
      }
      final Map<Class<?>, Integer> newContracts = new HashMap<>();
      processProviderContracts(provider, priorityOverride, isBuiltin, contracts, newContracts);
      providerClasses.add(provider);
      classContracts.put(provider, newContracts);
   }

   public Set<Class<?>> getMutableProviderClasses() {
      return providerClasses;
   }

   private void processProviderContracts(Class provider, Integer priorityOverride, boolean isBuiltin,
         Map<Class<?>, Integer> contracts, Map<Class<?>, Integer> newContracts)
   {
      clientHelper.processProviderContracts(provider, priorityOverride, isBuiltin, contracts, newContracts);
      serverHelper.processProviderContracts(provider, priorityOverride, isBuiltin, contracts, newContracts);

      if (Utils.isA(provider, ParamConverterProvider.class, contracts))
      {
         int priority = Utils.getPriority(priorityOverride, contracts, ParamConverterProvider.class, provider);
         addParameterConverterProvider(provider, isBuiltin, priority);
         newContracts.put(ParamConverterProvider.class, priority);
      }
      if (Utils.isA(provider, ContextResolver.class, contracts))
      {
         try
         {
            int priority = Utils.getPriority(priorityOverride, contracts, ContextResolver.class, provider);
            addContextResolver(provider, isBuiltin, priority);
            newContracts.put(ContextResolver.class, priority);
         }
         catch (Exception e)
         {
            throw new RuntimeException(Messages.MESSAGES.unableToInstantiateContextResolver(), e);
         }
      }
      if (Utils.isA(provider, ContextInjector.class, contracts))
      {
         try
         {
            addContextInjector(provider);
            int priority = Utils.getPriority(priorityOverride, contracts, ContextInjector.class, provider);
            newContracts.put(ContextInjector.class, priority);
         }
         catch (Exception e)
         {
            throw new RuntimeException(Messages.MESSAGES.unableToInstantiateContextInjector(), e);
         }
      }
      if (Utils.isA(provider, StringParameterUnmarshaller.class, contracts))
      {
         addStringParameterUnmarshaller(provider);
         int priority = Utils.getPriority(priorityOverride, contracts, StringParameterUnmarshaller.class, provider);
         newContracts.put(StringParameterUnmarshaller.class, priority);
      }
      if (Utils.isA(provider, InjectorFactory.class, contracts))
      {
         try
         {
            addInjectorFactory(provider);
            newContracts.put(InjectorFactory.class, 0);
         }
         catch (Exception e)
         {
            throw new RuntimeException(e);
         }
      }
      if (Utils.isA(provider, Feature.class, contracts))
      {
         ConstrainedTo constrainedTo = (ConstrainedTo) provider.getAnnotation(ConstrainedTo.class);
         int priority = Utils.getPriority(priorityOverride, contracts, Feature.class, provider);
         if (constrainedTo == null || constrainedTo.value() == getRuntimeType())
         {
            addFeature(provider);
         }
         newContracts.put(Feature.class, priority);
      }
      if (Utils.isA(provider, ResourceClassProcessor.class, contracts))
      {
         int priority = Utils.getPriority(priorityOverride, contracts, ResourceClassProcessor.class, provider);
         addResourceClassProcessor(provider, priority);
         newContracts.put(ResourceClassProcessor.class, priority);
      }
      if (Utils.isA(provider, HeaderDelegate.class, contracts))
      {
         addHeaderDelegate(provider);
      }
      if (Utils.isA(provider, ThreadContext.class, contracts)) {
         ResteasyContext.computeIfAbsent(ThreadContexts.class, ThreadContexts::new)
            .add(createProviderInstance((Class<? extends ThreadContext>) provider));
      }
   }

   public void addHeaderDelegate(Class provider) {
      Type[] headerTypes = Types.getActualTypeArgumentsOfAnInterface(provider, HeaderDelegate.class);
      if (headerTypes.length == 0)
      {
         LogMessages.LOGGER.cannotRegisterheaderDelegate(provider);
      }
      else
      {
         Class<?> headerClass = Types.getRawType(headerTypes[0]);
         addHeaderDelegate(provider, headerClass);
      }
   }

   public ClientHelper getClientHelper() {
      return clientHelper;
   }

   public ServerHelper getServerHelper() {
      return serverHelper;
   }

   public void addHeaderDelegate(Class<? extends HeaderDelegate> provider, Class<?> headerClass) {
      HeaderDelegate<?> delegate = createProviderInstance(provider);
      addHeaderDelegate(headerClass, delegate);
   }

   public void addFeature(Class<? extends Feature> provider) {
      Feature feature = injectedInstance(provider);
      if (feature.configure(new FeatureContextDelegate(this)))
      {
         enabledFeatures.add(feature);
      }
   }

   public void addInjectorFactory(Class provider) throws InstantiationException, IllegalAccessException {
      this.injectorFactory = (InjectorFactory) provider.newInstance();
   }

   public void addContextInjector(Class provider) {
      addContextInjector(createProviderInstance((Class<? extends ContextInjector>) provider), provider);
   }

   public void addContextResolver(Class provider, boolean isBuiltin, int priority) {
      addContextResolver(createProviderInstance((Class<? extends ContextResolver>)provider), priority, provider, isBuiltin);
   }

   public void addParameterConverterProvider(Class provider, boolean isBuiltin, int priority) {
      ParamConverterProvider paramConverterProvider = (ParamConverterProvider) injectedInstance(provider);
      injectProperties(provider);
      copyParamConvertsIfNeeded();
      sortedParamConverterProviders
            .add(new ExtSortedKey<>(null, paramConverterProvider, provider, priority, isBuiltin));
   }

   private void copyParamConvertsIfNeeded() {
      if (attachedParamConverterProviders) {
         sortedParamConverterProviders = Collections.synchronizedSortedSet(new TreeSet<>(sortedParamConverterProviders));
         attachedParamConverterProviders = false;
      }
   }

   /**
    * Register a @Provider object.  Can be a MessageBodyReader/Writer or ExceptionMapper.
    *
    * @param provider provider instance
    */
   public void registerProviderInstance(Object provider)
   {
      registerProviderInstance(provider, null, null, false);
   }

   public void registerProviderInstance(Object provider, Map<Class<?>, Integer> contracts, Integer priorityOverride,
         boolean builtIn)
   {
      Class<?> providerClass = provider.getClass();
      Map<Class<?>, Map<Class<?>, Integer>> classContracts = getClassContracts();
      if (classContracts.containsKey(providerClass))
      {
         LogMessages.LOGGER.providerInstanceAlreadyRegistered(providerClass.getName());
         return;
      }
      Map<Class<?>, Integer> newContracts = new HashMap<>();
      processProviderInstanceContracts(provider, contracts, priorityOverride, builtIn, newContracts);
      providerInstances.add(provider);
      classContracts.put(providerClass, newContracts);
   }

   private void processProviderInstanceContracts(Object provider, Map<Class<?>, Integer> contracts,
         Integer priorityOverride, boolean builtIn, Map<Class<?>, Integer> newContracts)
   {
      clientHelper.processProviderInstanceContracts(provider, contracts, priorityOverride, builtIn, newContracts);
      serverHelper.processProviderInstanceContracts(provider, contracts, priorityOverride, builtIn, newContracts);

      if (Utils.isA(provider, ParamConverterProvider.class, contracts))
      {
         injectProperties(provider);
         int priority = Utils.getPriority(priorityOverride, contracts, ParamConverterProvider.class, provider.getClass());
         copyParamConvertsIfNeeded();
         sortedParamConverterProviders.add(
               new ExtSortedKey<>(null, (ParamConverterProvider) provider, provider.getClass(), priority, builtIn));
         newContracts.put(ParamConverterProvider.class, priority);
      }
      if (Utils.isA(provider, ContextResolver.class, contracts))
      {
         try
         {
            int priority = Utils.getPriority(priorityOverride, contracts, ContextResolver.class, provider.getClass());
            addContextResolver((ContextResolver) provider, priority, provider.getClass(), false);
            newContracts.put(ContextResolver.class, priority);
         }
         catch (Exception e)
         {
            throw new RuntimeException(Messages.MESSAGES.unableToInstantiateContextResolver(), e);
         }
      }
      if (Utils.isA(provider, ContextInjector.class, contracts))
      {
         try
         {
            addContextInjector((ContextInjector) provider, provider.getClass());
            int priority = Utils.getPriority(priorityOverride, contracts, ContextInjector.class, provider.getClass());
            newContracts.put(ContextInjector.class, priority);
         }
         catch (Exception e)
         {
            throw new RuntimeException(Messages.MESSAGES.unableToInstantiateContextInjector(), e);
         }
      }
      if (Utils.isA(provider, InjectorFactory.class, contracts))
      {
         this.injectorFactory = (InjectorFactory) provider;
         newContracts.put(InjectorFactory.class, 0);
      }
      if (Utils.isA(provider, Feature.class, contracts))
      {
         Feature feature = (Feature) provider;
         Utils.injectProperties(this, provider.getClass(), provider);
         ConstrainedTo constrainedTo = (ConstrainedTo) provider.getClass().getAnnotation(ConstrainedTo.class);
         if (constrainedTo == null || constrainedTo.value() == getRuntimeType())
         {
            if (feature.configure(new FeatureContextDelegate(this)))
            {
               enabledFeatures.add(feature);
            }
         }
         int priority = Utils.getPriority(priorityOverride, contracts, Feature.class, provider.getClass());
         newContracts.put(Feature.class, priority);

      }
      if (Utils.isA(provider, ResourceClassProcessor.class, contracts))
      {
         int priority = Utils.getPriority(priorityOverride, contracts, ResourceClassProcessor.class, provider.getClass());
         addResourceClassProcessor((ResourceClassProcessor) provider, priority);
         newContracts.put(ResourceClassProcessor.class, priority);
      }
      if (Utils.isA(provider, HeaderDelegate.class, contracts))
      {
         Type[] headerTypes = Types.getActualTypeArgumentsOfAnInterface(provider.getClass(), HeaderDelegate.class);
         if (headerTypes.length == 0)
         {
            LogMessages.LOGGER.cannotRegisterheaderDelegate(provider.getClass());
         }
         else
         {
            Class<?> headerClass = Types.getRawType(headerTypes[0]);
            addHeaderDelegate(headerClass, (HeaderDelegate) provider);
         }
      }
      if (Utils.isA(provider, ThreadContext.class, contracts)) {
         ResteasyContext.computeIfAbsent(ThreadContexts.class, ThreadContexts::new)
                 .add((ThreadContext<?>) provider);
      }
   }

   @Override
   public <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> type)
   {
      Class exceptionType = type;
      SortedKey<ExceptionMapper> mapper = null;
      Map<Class<?>, SortedKey<ExceptionMapper>> mappers = getSortedExceptionMappers();
      if (mappers == null) {
         return (ExceptionMapper<T>) DefaultExceptionMapper.INSTANCE;
      }
      while (mapper == null)
      {
         if (exceptionType == null)
            break;
         mapper = mappers.get(exceptionType);
         if (mapper == null)
            exceptionType = exceptionType.getSuperclass();
      }
      return mapper != null ? mapper.getObj() : (ExceptionMapper<T>) DefaultExceptionMapper.INSTANCE;
   }

   public <T extends Throwable> ExceptionMapper<T> getExceptionMapperForClass(Class<T> type)
   {
      Map<Class<?>, SortedKey<ExceptionMapper>> mappers = getSortedExceptionMappers();
      if (mappers == null) return null;
      SortedKey<ExceptionMapper> mapper = mappers.get(type);
      return mapper != null ? mapper.getObj() : null;
   }

   //   @Override
   public <T> AsyncResponseProvider<T> getAsyncResponseProvider(Class<T> type)
   {
      Class asyncType = type;
      AsyncResponseProvider<T> mapper = null;
      while (mapper == null)
      {
         if (asyncType == null)
            break;
         Map<Class<?>, AsyncResponseProvider> asyncResponseProviders = getAsyncResponseProviders();
         if (asyncResponseProviders != null) mapper = asyncResponseProviders.get(asyncType);
         if (mapper == null)
            asyncType = asyncType.getSuperclass();
      }
      return mapper;
   }

   public <T> AsyncClientResponseProvider<T> getAsyncClientResponseProvider(Class<T> type)
   {
      Class asyncType = type;
      AsyncClientResponseProvider<T> mapper = null;
      while (mapper == null)
      {
         if (asyncType == null)
            break;
         Map<Class<?>, AsyncClientResponseProvider> asyncClientResponseProviders = getAsyncClientResponseProviders();
         if (asyncClientResponseProviders != null) mapper = asyncClientResponseProviders.get(asyncType);
         if (mapper == null)
            asyncType = asyncType.getSuperclass();
      }
      return mapper;
   }

   // @Override
   public <T> AsyncStreamProvider<T> getAsyncStreamProvider(Class<T> type)
   {
      Class asyncType = type;
      AsyncStreamProvider<T> mapper = null;
      while (mapper == null)
      {
         if (asyncType == null)
            break;
         Map<Class<?>, AsyncStreamProvider> asyncStreamProviders = getAsyncStreamProviders();
         if (asyncStreamProviders != null) mapper = asyncStreamProviders.get(asyncType);
         if (mapper == null)
            asyncType = asyncType.getSuperclass();
      }
      return mapper;
   }

   public MediaType getConcreteMediaTypeFromMessageBodyWriters(Class<?> type, Type genericType,
         Annotation[] annotations, MediaType mediaType)
   {
      List<SortedKey<MessageBodyWriter>> writers = getServerMessageBodyWriters().getPossible(mediaType, type);
      for (SortedKey<MessageBodyWriter> writer : writers)
      {
         if (writer.getObj().isWriteable(type, genericType, annotations, mediaType))
         {
            MessageBodyWriter mbw = writer.getObj();
            Class writerType = Types.getTemplateParameterOfInterface(mbw.getClass(), MessageBodyWriter.class);
            if (writerType == null || writerType.equals(Object.class) || !writerType.isAssignableFrom(type))
               continue;
            Produces produces = mbw.getClass().getAnnotation(Produces.class);
            if (produces == null)
               continue;
            for (String produce : produces.value())
            {
               MediaType mt = MediaType.valueOf(produce);
               if (mt.isWildcardType() || mt.isWildcardSubtype())
                  continue;
               return mt;
            }
         }
      }
      return null;
   }

   public Map<MessageBodyWriter<?>, Class<?>> getPossibleMessageBodyWritersMap(Class type, Type genericType,
         Annotation[] annotations, MediaType accept)
   {
      Map<MessageBodyWriter<?>, Class<?>> map = new HashMap<MessageBodyWriter<?>, Class<?>>();
      MediaTypeMap<SortedKey<MessageBodyWriter>> serverMessageBodyWriters = getServerMessageBodyWriters();
      if (serverMessageBodyWriters == null) return map;
      List<SortedKey<MessageBodyWriter>> writers = serverMessageBodyWriters.getPossible(accept, type);
      for (SortedKey<MessageBodyWriter> writer : writers)
      {
         if (writer.getObj().isWriteable(type, genericType, annotations, accept))
         {
            Class<?> mbwc = writer.getObj().getClass();
            if (!mbwc.isInterface() && mbwc.getSuperclass() != null && !mbwc.getSuperclass().equals(Object.class)
                  && mbwc.isSynthetic())
            {
               mbwc = mbwc.getSuperclass();
            }
            Class writerType = Types.getTemplateParameterOfInterface(mbwc, MessageBodyWriter.class);
            if (writerType == null || !writerType.isAssignableFrom(type))
               continue;
            map.put(writer.getObj(), writerType);
         }
      }
      return map;
   }

   // use the tracingLogger enabled version please
   @Deprecated
   public <T> MessageBodyWriter<T> getServerMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters = getServerMessageBodyWriters();
      return resolveMessageBodyWriter(type, genericType, annotations, mediaType, availableWriters);
   }

   public <T> MessageBodyWriter<T> getServerMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType, RESTEasyTracingLogger tracingLogger)
   {

      MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters = getServerMessageBodyWriters();
      return resolveMessageBodyWriter(type, genericType, annotations, mediaType, availableWriters, tracingLogger);
   }

   /**
    * Always gets server MBW.
    *
    * @param type        the class of the object that is to be written.
    * @param genericType the type of object to be written. E.g. if the
    *                    message body is to be produced from a field, this will be
    *                    the declared type of the field as returned by {@code Field.getGenericType}.
    * @param annotations an array of the annotations on the declaration of the
    *                    artifact that will be written. E.g. if the
    *                    message body is to be produced from a field, this will be
    *                    the annotations on that field returned by
    *                    {@code Field.getDeclaredAnnotations}.
    * @param mediaType   the media type of the data that will be written.
    * @param <T> type
    * @return message writer
    */
   public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters = getServerMessageBodyWriters();
      MessageBodyWriter<T> writer = resolveMessageBodyWriter(type, genericType, annotations, mediaType,
            availableWriters);
      if (writer != null)
         LogMessages.LOGGER.debugf("MessageBodyWriter: %s", writer.getClass().getName());
      return writer;
   }

   public <T> MessageBodyWriter<T> getClientMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType)
   {
      MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters = getClientMessageBodyWriters();
      return resolveMessageBodyWriter(type, genericType, annotations, mediaType, availableWriters);
   }

   @Deprecated
   private <T> MessageBodyWriter<T> resolveMessageBodyWriter(Class<T> type, Type genericType, Annotation[] annotations,
         MediaType mediaType, MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters)
   {
      if (availableWriters == null) return null;
      List<SortedKey<MessageBodyWriter>> writers = availableWriters.getPossible(mediaType, type);
      /*
      logger.info("*******   getMessageBodyWriter(" + type.getName() + ", " + mediaType.toString() + ")****");
      for (SortedKey<MessageBodyWriter> writer : writers)
      {
         logger.info("     possible writer: " + writer.obj.getClass().getName());
      }
      */

      for (SortedKey<MessageBodyWriter> writer : writers)
      {
         if (writer.getObj().isWriteable(type, genericType, annotations, mediaType))
         {
            LogMessages.LOGGER.debugf("MessageBodyWriter: %s", writer.getClass().getName());
            //logger.info("   picking: " + writer.obj.getClass().getName());
            return (MessageBodyWriter<T>) writer.getObj();
         }
      }
      return null;
   }

   private <T> MessageBodyWriter<T> resolveMessageBodyWriter(Class<T> type, Type genericType,
         Annotation[] annotations, MediaType mediaType, MediaTypeMap<SortedKey<MessageBodyWriter>> availableWriters,
         RESTEasyTracingLogger tracingLogger)
   {
      if (availableWriters == null) return null;
      List<SortedKey<MessageBodyWriter>> writers = availableWriters.getPossible(mediaType, type);

      if (tracingLogger.isLogEnabled("MBW_FIND"))
      {
         tracingLogger.log("MBW_FIND", type.getName(),
               (genericType instanceof Class ? ((Class) genericType).getName() : genericType), mediaType,
               java.util.Arrays.toString(annotations));
      }

      MessageBodyWriter<T> result = null;

      Iterator<SortedKey<MessageBodyWriter>> iterator = writers.iterator();

      while (iterator.hasNext())
      {
         final SortedKey<MessageBodyWriter> writer = iterator.next();
         if (writer.getObj().isWriteable(type, genericType, annotations, mediaType))
         {
            LogMessages.LOGGER.debugf("MessageBodyWriter: %s", writer.getClass().getName());
            result = (MessageBodyWriter<T>) writer.getObj();
            tracingLogger.log("MBW_SELECTED", result);
            break;
         }
         tracingLogger.log("MBW_NOT_WRITEABLE", result);
      }

      if (tracingLogger.isLogEnabled("MBW_SKIPPED"))
      {
         while (iterator.hasNext())
         {
            final SortedKey<MessageBodyWriter> writer = iterator.next();
            tracingLogger.log("MBW_SKIPPED", writer.getObj());
         }
      }
      return result;
   }

   /**
    * This is a spec method that is unsupported.  It is an optional method anyways.
    *
    * @param applicationConfig application
    * @param endpointType endpoint type
    * @return endpoint
    * @throws IllegalArgumentException if applicationConfig is null
    * @throws UnsupportedOperationException allways throw since this method is not supported
    */
   public <T> T createEndpoint(Application applicationConfig, Class<T> endpointType)
         throws IllegalArgumentException, UnsupportedOperationException
   {
      if (applicationConfig == null)
         throw new IllegalArgumentException(Messages.MESSAGES.applicationParamNull());
      throw new UnsupportedOperationException();
   }

   public <T> ContextResolver<T> getContextResolver(Class<T> contextType, MediaType mediaType)
   {
      final List<ContextResolver> resolvers = getContextResolvers(contextType, mediaType);
      if (resolvers == null)
         return null;
      if (resolvers.size() == 1)
         return resolvers.get(0);
      return new ContextResolver<T>()
      {
         public T getContext(Class type)
         {
            for (ContextResolver resolver : resolvers)
            {
               Object rtn = resolver.getContext(type);
               if (rtn != null)
                  return (T) rtn;
            }
            return null;
         }
      };
   }

   /**
    * Create an instance of a class using provider allocation rules of the specification as well as the InjectorFactory
    * only does constructor injection.
    *
    * @param clazz class
    * @param <T> type
    * @return provider instance of type T
    */
   public <T> T createProviderInstance(Class<? extends T> clazz)
   {
      return Utils.createProviderInstance(this, clazz);
   }

   /**
    * Property and constructor injection using the InjectorFactory.
    *
    * @param clazz class
    * @param <T> type
    * @return instance of type T
    */
   public <T> T injectedInstance(Class<? extends T> clazz)
   {
      Constructor<?> constructor = PickConstructor.pickSingletonConstructor(clazz);
      ConstructorInjector constructorInjector = getInjectorFactory().createConstructor(constructor, this);
      Object obj =  constructorInjector.construct(false);
      PropertyInjector propertyInjector = getInjectorFactory().createPropertyInjector(clazz, this);
      if (obj instanceof CompletionStage) {
         CompletionStage<Object> stage = (CompletionStage<Object>)obj;
         return (T)stage.thenCompose(target -> {
            CompletionStage<Void> propertyStage = propertyInjector.inject(target, false);
            if (propertyStage != null) {
               return propertyStage
                       .thenApply(v -> target);
            } else {
               return CompletableFuture.completedFuture(target);
            }
         }).toCompletableFuture().getNow(null);
      }
      CompletionStage<Void> propertyStage = propertyInjector.inject(obj, false);
      if (propertyStage == null) return (T)obj;
      return (T) propertyStage.thenApply(v -> obj).toCompletableFuture().getNow(null);
   }

   /**
    * Property and constructor injection using the InjectorFactory.
    *
    * @param clazz class
    * @param request http request
    * @param response http response
    * @param <T> type
    * @return instance of type T
    */
   public <T> T injectedInstance(Class<? extends T> clazz, HttpRequest request, HttpResponse response)
   {
      Constructor<?> constructor = PickConstructor.pickSingletonConstructor(clazz);
      Object obj = null;
      if (constructor == null)
      {
         throw new IllegalArgumentException(Messages.MESSAGES.unableToFindPublicConstructorForClass(clazz.getName()));
      }
      else
      {
         ConstructorInjector constructorInjector = getInjectorFactory().createConstructor(constructor, this);
         obj = constructorInjector.construct(request, response, false);
         if (obj instanceof CompletionStage) {
            obj = ((CompletionStage<Object>)obj).toCompletableFuture().getNow(null);
         }
      }
      PropertyInjector propertyInjector = getInjectorFactory().createPropertyInjector(clazz, this);

      CompletionStage<Void> propertyStage = propertyInjector.inject(request, response, obj, false);
      if (propertyStage != null) propertyStage.toCompletableFuture().getNow(null);
      return (T) obj;
   }

   // Configurable
   public Map<String, Object> getMutableProperties()
   {
      return properties;
   }

   @Override
   public Map<String, Object> getProperties()
   {
      return Collections.unmodifiableMap(properties);
   }

   @Override
   public Object getProperty(String name)
   {
      return properties.get(name);
   }

   public ResteasyProviderFactory setProperties(Map<String, Object> properties)
   {
      this.properties = new SnapshotMap<>(properties, false, lockSnapshots, false);
      return this;
   }

   @Override
   public ResteasyProviderFactory property(String name, Object value)
   {
      if (value == null)
         properties.remove(name);
      else
         properties.put(name, value);
      return this;
   }

   public Collection<Feature> getEnabledFeatures()
   {
      return Collections.unmodifiableSet(enabledFeatures);
   }

   @Override
   public ResteasyProviderFactory register(Class<?> providerClass)
   {
      registerProvider(providerClass);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Object provider)
   {
      registerProviderInstance(provider);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Class<?> componentClass, int priority)
   {
      registerProvider(componentClass, priority, false, null);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Class<?> componentClass, Class<?>... contracts)
   {
      if (contracts == null || contracts.length == 0)
      {
         LogMessages.LOGGER.attemptingToRegisterEmptyContracts(componentClass.getName());
         return this;
      }
      Map<Class<?>, Integer> cons = new HashMap<Class<?>, Integer>();
      for (Class<?> contract : contracts)
      {
         if (!contract.isAssignableFrom(componentClass))
         {
            LogMessages.LOGGER.attemptingToRegisterUnassignableContract(componentClass.getName());
            return this;
         }
         cons.put(contract, Priorities.USER);
      }
      registerProvider(componentClass, null, false, cons);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Object component, int priority)
   {
      registerProviderInstance(component, null, priority, false);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Object component, Class<?>... contracts)
   {
      if (contracts == null || contracts.length == 0)
      {
         LogMessages.LOGGER.attemptingToRegisterEmptyContracts(component.getClass().getName());
         return this;
      }
      Map<Class<?>, Integer> cons = new HashMap<Class<?>, Integer>();
      for (Class<?> contract : contracts)
      {
         if (!contract.isAssignableFrom(component.getClass()))
         {
            LogMessages.LOGGER.attemptingToRegisterUnassignableContract(component.getClass().getName());
            return this;
         }
         cons.put(contract, Priorities.USER);
      }
      registerProviderInstance(component, cons, null, false);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Class<?> componentClass, Map<Class<?>, Integer> contracts)
   {
      for (Class<?> contract : contracts.keySet())
      {
         if (!contract.isAssignableFrom(componentClass))
         {
            LogMessages.LOGGER.attemptingToRegisterUnassignableContract(componentClass.getName());
            return this;
         }
      }
      registerProvider(componentClass, null, false, contracts);
      return this;
   }

   @Override
   public ResteasyProviderFactory register(Object component, Map<Class<?>, Integer> contracts)
   {
      for (Class<?> contract : contracts.keySet())
      {
         if (!contract.isAssignableFrom(component.getClass()))
         {
            LogMessages.LOGGER.attemptingToRegisterUnassignableContract(component.getClass().getName());
            return this;
         }
      }
      registerProviderInstance(component, contracts, null, false);
      return this;
   }

   @Override
   public Configuration getConfiguration()
   {
      return this;
   }

   @Override
   public RuntimeType getRuntimeType()
   {
      return RuntimeType.SERVER;
   }

   @Override
   public Collection<String> getPropertyNames()
   {
      return getProperties().keySet();
   }

   @Override
   public boolean isEnabled(Feature feature)
   {
      return getEnabledFeatures().contains(feature);
   }

   @Override
   public boolean isEnabled(Class<? extends Feature> featureClass)
   {
      Collection<Feature> enabled = getEnabledFeatures();
      //logger.info("isEnabled(Class): " + featureClass.getName() + " # enabled: " + enabled.size());
      if (enabled == null)
         return false;
      for (Feature feature : enabled)
      {
         //logger.info("  looking at: " + feature.getClass());
         if (featureClass.equals(feature.getClass()))
         {
            //logger.info("   found: " + featureClass.getName());
            return true;
         }
      }
      //logger.info("not enabled class: " + featureClass.getName());
      return false;
   }

   @Override
   public boolean isRegistered(Object component)
   {
      return getProviderInstances().contains(component);
   }

   @Override
   public boolean isRegistered(Class<?> componentClass)
   {
      return getClassContracts().containsKey(componentClass);
   }

   @Override
   public Map<Class<?>, Integer> getContracts(Class<?> componentClass)
   {
      Map<Class<?>, Integer> classIntegerMap = classContracts.get(componentClass);
      if (classIntegerMap == null)
         return Collections.emptyMap();
      return classIntegerMap;
   }

   @Override
   public Set<Class<?>> getClasses()
   {
      Set<Class<?>> providerClasses = getProviderClasses();
      return (providerClasses == null || providerClasses.isEmpty())
            ? Collections.emptySet()
            : Collections.unmodifiableSet(providerClasses);
   }

   @Override
   public Set<Object> getInstances()
   {
      Set<Object> providerInstances = getProviderInstances();
      return (providerInstances == null || providerInstances.isEmpty())
            ? Collections.emptySet()
            : Collections.unmodifiableSet(providerInstances);
   }

   @Override
   public Link.Builder createLinkBuilder()
   {
      return Utils.createLinkBuilder();
   }

   @Override
   public boolean hasProperty(final String name) {
      return properties.containsKey(name);
   }

   @Override
   public SeBootstrap.Configuration.Builder createConfigurationBuilder() {
      return ResteasySeConfiguration.builder();
   }

   @Override
   public CompletionStage<SeBootstrap.Instance> bootstrap(final Application application,
                                                          final SeBootstrap.Configuration configuration) {
      return ResteasySeInstance.create(Objects.requireNonNull(application, Messages.MESSAGES.nullParameter("application")),
              configuration);
   }

   @Override
   public CompletionStage<SeBootstrap.Instance> bootstrap(final Class<? extends Application> clazz,
                                                          final SeBootstrap.Configuration configuration) {
      return ResteasySeInstance.create(Objects.requireNonNull(clazz, Messages.MESSAGES.nullParameter("clazz")),
              configuration);
   }

   @Override
   public EntityPart.Builder createEntityPartBuilder(final String partName) throws IllegalArgumentException {
      throw new UnsupportedOperationException("Pending implementation");
   }

   public <I extends RxInvoker> RxInvokerProvider<I> getRxInvokerProvider(Class<I> clazz)
   {
      for (Entry<Class<?>, Map<Class<?>, Integer>> entry : classContracts.entrySet())
      {
         if (entry.getValue().containsKey(RxInvokerProvider.class))
         {
            RxInvokerProvider<?> rip = (RxInvokerProvider<?>) createProviderInstance(entry.getKey());
            if (rip.isProviderFor(clazz))
            {
               return (RxInvokerProvider<I>) rip;
            }
         }
      }
      return null;
   }

   public RxInvokerProvider<?> getRxInvokerProviderFromReactiveClass(Class<?> clazz)
   {
      return clientHelper.getRxInvokerProviderFromReactiveClass(clazz);
   }

   public boolean isReactive(Class<?> clazz)
   {
      return clientHelper.isReactive(clazz);
   }

   public void addResourceClassProcessor(Class<ResourceClassProcessor> processorClass, int priority)
   {
      ResourceClassProcessor processor = createProviderInstance(processorClass);
      addResourceClassProcessor(processor, priority);
   }

   private void addResourceClassProcessor(ResourceClassProcessor processor, int priority)
   {
      resourceBuilder.registerResourceClassProcessor(processor, priority);
   }

   public ResourceBuilder getResourceBuilder()
   {
      return resourceBuilder;
   }

   public <T> T getContextData(Class<T> type)
   {
      return ResteasyContext.getContextData(type);
   }

   public void initializeClientProviders(ResteasyProviderFactory factory)
   {
      clientHelper.initializeClientProviders(factory);
   }

   public void injectProperties(Object obj)
   {
      Utils.injectProperties(this, obj);
   }

   public void injectProperties(Object obj, HttpRequest request, HttpResponse response)
   {
      Utils.injectProperties(this, obj, request, response);
   }

   public StatisticsController getStatisticsController() {
      return statisticsController;
   }

   @Override
   public ExceptionMapper<Throwable> getThrowableExceptionMapper() {
      final ExceptionMapper<Throwable> result = getExceptionMapperForClass(Throwable.class);
      return result != null ? result : DefaultExceptionMapper.INSTANCE;
   }

   @Override
   protected boolean isOnServer() {
      return ResteasyContext.searchContextData(Dispatcher.class) != null;
   }
}
