package org.jboss.resteasy.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import jakarta.ws.rs.container.DynamicFeature;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Configurable;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.Providers;

import org.jboss.resteasy.core.providerfactory.ResteasyProviderFactoryImpl;
import org.jboss.resteasy.plugins.interceptors.RoleBasedSecurityFeature;
import org.jboss.resteasy.plugins.providers.JaxrsServerFormUrlEncodedProvider;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.plugins.providers.ServerFormUrlEncodedProvider;
import org.jboss.resteasy.plugins.server.resourcefactory.JndiComponentResourceFactory;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.Dispatcher;
import org.jboss.resteasy.spi.InjectorFactory;
import org.jboss.resteasy.spi.PriorityServiceLoader;
import org.jboss.resteasy.spi.PropertyInjector;
import org.jboss.resteasy.spi.Registry;
import org.jboss.resteasy.spi.ResourceFactory;
import org.jboss.resteasy.spi.ResteasyConfiguration;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.config.ConfigurationFactory;
import org.jboss.resteasy.spi.config.Options;
import org.jboss.resteasy.spi.metadata.ResourceBuilder;
import org.jboss.resteasy.util.GetRestful;

/**
 * This class is used to configure and initialize the core components of RESTEasy.
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class ResteasyDeploymentImpl implements ResteasyDeployment {
    protected boolean widerRequestMatching;
    protected boolean useContainerFormParams = false;
    protected boolean deploymentSensitiveFactoryEnabled = false;
    protected boolean asyncJobServiceEnabled = false;
    protected boolean addCharset = true;
    protected int asyncJobServiceMaxJobResults = 100;
    protected long asyncJobServiceMaxWait = 300000;
    protected int asyncJobServiceThreadPoolSize = 100;
    protected String asyncJobServiceBasePath = "/asynch/jobs";
    protected String applicationClass;
    protected String injectorFactoryClass;
    protected InjectorFactory injectorFactory;
    protected Application application;
    protected boolean registerBuiltin = true;
    protected List<String> scannedResourceClasses;
    protected List<String> scannedProviderClasses;
    protected List<String> scannedJndiComponentResources;
    protected Map<String, List<String>> scannedResourceClassesWithBuilder;
    protected List<String> jndiComponentResources;
    protected List<String> providerClasses;
    @SuppressWarnings("rawtypes")
    protected List<Class> actualProviderClasses;
    protected List<Object> providers;
    private final Set<String> disabledProviders;
    protected boolean securityEnabled = false;
    protected List<String> jndiResources;
    protected List<String> resourceClasses;
    protected List<String> unwrappedExceptions;
    @SuppressWarnings("rawtypes")
    protected List<Class> actualResourceClasses;
    protected List<ResourceFactory> resourceFactories;
    protected List<Object> resources;
    protected Map<String, String> mediaTypeMappings;
    protected Map<String, String> languageExtensions;
    @SuppressWarnings("rawtypes")
    protected Map<Class, Object> defaultContextObjects;
    protected Map<String, String> constructedDefaultContextObjects;
    protected Registry registry;
    protected Dispatcher dispatcher;
    protected ResteasyProviderFactory providerFactory;
    protected ThreadLocalResteasyProviderFactory threadLocalProviderFactory;
    protected String paramMapping;
    protected Map<String, Object> properties;
    protected boolean statisticsEnabled;

    @SuppressWarnings("rawtypes")
    public ResteasyDeploymentImpl() {
        scannedResourceClasses = new ArrayList<String>();
        scannedProviderClasses = new ArrayList<String>();
        scannedJndiComponentResources = new ArrayList<String>();
        scannedResourceClassesWithBuilder = new HashMap<>();
        jndiComponentResources = new ArrayList<String>();
        providerClasses = new ArrayList<String>();
        actualProviderClasses = new ArrayList<Class>();
        providers = new ArrayList<Object>();
        disabledProviders = new HashSet<String>();
        jndiResources = new ArrayList<String>();
        resourceClasses = new ArrayList<String>();
        unwrappedExceptions = new ArrayList<String>();
        actualResourceClasses = new ArrayList<Class>();
        resourceFactories = new ArrayList<ResourceFactory>();
        resources = new ArrayList<Object>();
        mediaTypeMappings = new HashMap<String, String>();
        languageExtensions = new HashMap<String, String>();
        defaultContextObjects = new HashMap<Class, Object>();
        constructedDefaultContextObjects = new HashMap<String, String>();
        properties = new TreeMap<String, Object>();
    }

    public ResteasyDeploymentImpl(final boolean quarkus) {
        disabledProviders = new HashSet<>();
    }

    public void start() {
        try {
            startInternal();
        } finally {
            ThreadLocalResteasyProviderFactory.pop();
        }
    }

    private void startInternal() {
        initializeFactory();
        initializeDispatcher();
        pushContext();

        try {
            initializeObjects();

            if (securityEnabled) {
                providerFactory.register(RoleBasedSecurityFeature.class);
            }

            if (registerBuiltin) {
                providerFactory.setRegisterBuiltins(true);
                RegisterBuiltin.register(providerFactory, disabledProviders);

                // having problems using form parameters from container for a couple of TCK tests.  I couldn't figure out
                // why, specifically:
                // com/sun/ts/tests/jaxrs/spec/provider/standardhaspriority/JAXRSClient.java#readWriteMapProviderTest_from_standalone                                               Failed. Test case throws exception: [JAXRSCommonClient] null failed!  Check output for cause of failure.
                // com/sun/ts/tests/jaxrs/spec/provider/standardwithjaxrsclient/JAXRSClient.java#mapElementProviderTest_from_standalone                                             Failed. Test case throws exception: returned MultivaluedMap is null
                providerFactory.registerProviderInstance(new ServerFormUrlEncodedProvider(useContainerFormParams), null, null,
                        true);
                providerFactory.registerProviderInstance(new JaxrsServerFormUrlEncodedProvider(useContainerFormParams), null,
                        null, true);
            } else {
                providerFactory.setRegisterBuiltins(false);
            }

            // register all providers
            registration();

            registerMappers();
            ((ResteasyProviderFactoryImpl) providerFactory).lockSnapshots();
        } finally {
            ResteasyContext.removeContextDataLevel();
        }
    }

    protected void registerMappers() {
        if (paramMapping != null) {
            providerFactory.getContainerRequestFilterRegistry()
                    .registerSingleton(new AcceptParameterHttpPreprocessor(paramMapping));
        }

        AcceptHeaderByFileSuffixFilter suffixNegotiationFilter = null;
        if (mediaTypeMappings != null && !mediaTypeMappings.isEmpty()) {
            Map<String, MediaType> extMap = new HashMap<String, MediaType>();
            for (Map.Entry<String, String> ext : mediaTypeMappings.entrySet()) {
                String value = ext.getValue();
                extMap.put(ext.getKey().trim(), MediaType.valueOf(value.trim()));
            }

            if (suffixNegotiationFilter == null) {
                suffixNegotiationFilter = new AcceptHeaderByFileSuffixFilter();
                providerFactory.getContainerRequestFilterRegistry().registerSingleton(suffixNegotiationFilter);
            }
            suffixNegotiationFilter.setMediaTypeMappings(extMap);
        }

        if (languageExtensions != null && !languageExtensions.isEmpty()) {
            if (suffixNegotiationFilter == null) {
                suffixNegotiationFilter = new AcceptHeaderByFileSuffixFilter();
                providerFactory.getContainerRequestFilterRegistry().registerSingleton(suffixNegotiationFilter);
            }
            suffixNegotiationFilter.setLanguageMappings(languageExtensions);
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void pushContext() {
        // push context data so we can inject it
        Map contextDataMap = ResteasyContext.getContextDataMap();
        contextDataMap.putAll(dispatcher.getDefaultContextObjects());
    }

    protected void initializeObjects() {
        if (injectorFactory == null && injectorFactoryClass != null) {
            try {
                Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(injectorFactoryClass);
                injectorFactory = (InjectorFactory) clazz.newInstance();
            } catch (ClassNotFoundException cnfe) {
                throw new RuntimeException(Messages.MESSAGES.unableToFindInjectorFactory(), cnfe);
            } catch (Exception e) {
                throw new RuntimeException(Messages.MESSAGES.unableToInstantiateInjectorFactory(), e);
            }
        }
        if (injectorFactory != null) {
            providerFactory.setInjectorFactory(injectorFactory);
        }
        // feed context data map with constructed objects
        // see ResteasyContextParameters.RESTEASY_CONTEXT_OBJECTS
        if (constructedDefaultContextObjects != null && constructedDefaultContextObjects.size() > 0) {
            for (Map.Entry<String, String> entry : constructedDefaultContextObjects.entrySet()) {
                Class<?> key = null;
                try {
                    key = Thread.currentThread().getContextClassLoader().loadClass(entry.getKey());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(Messages.MESSAGES.unableToInstantiateContextObject(entry.getKey()), e);
                }
                Object obj = createFromInjectorFactory(entry.getValue(), providerFactory);
                LogMessages.LOGGER.creatingContextObject(entry.getKey(), entry.getValue());
                defaultContextObjects.put(key, obj);
                dispatcher.getDefaultContextObjects().put(key, obj);
                ResteasyContext.getContextDataMap().put(key, obj);

            }
        }

        if (applicationClass != null) {
            application = createApplication(applicationClass, dispatcher, providerFactory);
        }
    }

    protected void initializeDispatcher() {
        if (asyncJobServiceEnabled) {
            AsynchronousDispatcher asyncDispatcher;
            if (dispatcher == null) {
                asyncDispatcher = new AsynchronousDispatcher(providerFactory);
                dispatcher = asyncDispatcher;
            } else {
                asyncDispatcher = (AsynchronousDispatcher) dispatcher;
            }
            asyncDispatcher.setMaxCacheSize(asyncJobServiceMaxJobResults);
            asyncDispatcher.setMaxWaitMilliSeconds(asyncJobServiceMaxWait);
            asyncDispatcher.setThreadPoolSize(asyncJobServiceThreadPoolSize);
            asyncDispatcher.setBasePath(asyncJobServiceBasePath);
            if (unwrappedExceptions != null)
                asyncDispatcher.getUnwrappedExceptions().addAll(unwrappedExceptions);
            asyncDispatcher.start();
        } else {
            SynchronousDispatcher dis;
            if (dispatcher == null) {
                dis = new SynchronousDispatcher(providerFactory);
                dispatcher = dis;
            } else {
                dis = (SynchronousDispatcher) dispatcher;
            }
            if (unwrappedExceptions != null)
                dis.getUnwrappedExceptions().addAll(unwrappedExceptions);
        }
        registry = dispatcher.getRegistry();
        if (widerRequestMatching) {
            ((ResourceMethodRegistry) registry).setWiderMatching(widerRequestMatching);
        }

        if (defaultContextObjects != null)
            dispatcher.getDefaultContextObjects().putAll(defaultContextObjects);
        dispatcher.getDefaultContextObjects().put(Configurable.class, providerFactory);
        dispatcher.getDefaultContextObjects().put(Configuration.class, providerFactory);
        dispatcher.getDefaultContextObjects().put(Providers.class, providerFactory);
        dispatcher.getDefaultContextObjects().put(Registry.class, registry);
        dispatcher.getDefaultContextObjects().put(Dispatcher.class, dispatcher);
        dispatcher.getDefaultContextObjects().put(InternalDispatcher.class, InternalDispatcher.getInstance());
        dispatcher.getDefaultContextObjects().put(ResteasyDeployment.class, this);
    }

    protected void initializeFactory() {
        // Get the ResteasyConfiguration if it exists
        Object context = getDefaultContextObjects() == null ? null
                : getDefaultContextObjects().get(ResteasyConfiguration.class);
        // Check if the efault exception manager is enabled
        final boolean defaultExceptionManagerEnabled = Options.ENABLE_DEFAULT_EXCEPTION_MAPPER.getValue(
                (ResteasyConfiguration) context);
        // it is very important that each deployment create their own provider factory
        // this allows each WAR to have their own set of providers
        if (providerFactory == null)
            providerFactory = new ResteasyProviderFactoryImpl(defaultExceptionManagerEnabled);
        providerFactory.setRegisterBuiltins(registerBuiltin);
        providerFactory.getStatisticsController().setEnabled(statisticsEnabled);

        Object tracingText;
        Object thresholdText;

        org.jboss.resteasy.spi.config.Configuration config = ConfigurationFactory.getInstance()
                .getConfiguration((ResteasyConfiguration) context);
        tracingText = config.getOptionalValue(ResteasyContextParameters.RESTEASY_TRACING_TYPE, String.class).orElse(null);
        thresholdText = config.getOptionalValue(ResteasyContextParameters.RESTEASY_TRACING_THRESHOLD, String.class)
                .orElse(null);

        if (tracingText != null) {
            providerFactory.property(ResteasyContextParameters.RESTEASY_TRACING_TYPE, tracingText);
        } else {
            if (context != null) {
                tracingText = ((ResteasyConfiguration) context).getParameter(ResteasyContextParameters.RESTEASY_TRACING_TYPE);
                if (tracingText != null) {
                    providerFactory.property(ResteasyContextParameters.RESTEASY_TRACING_TYPE, tracingText);
                }
            }
        }

        if (thresholdText != null) {
            providerFactory.getMutableProperties().put(ResteasyContextParameters.RESTEASY_TRACING_THRESHOLD, thresholdText);
        } else {

            if (context != null) {
                thresholdText = ((ResteasyConfiguration) context)
                        .getInitParameter(ResteasyContextParameters.RESTEASY_TRACING_THRESHOLD);
                if (thresholdText != null) {
                    providerFactory.getMutableProperties().put(ResteasyContextParameters.RESTEASY_TRACING_THRESHOLD,
                            thresholdText);
                }
            }
        }

        if (deploymentSensitiveFactoryEnabled) {
            // the ThreadLocalResteasyProviderFactory pushes and pops this deployments parentProviderFactory
            // on a ThreadLocal stack.  This allows each application/WAR to have their own parentProviderFactory
            // and still be able to call ResteasyProviderFactory.getInstance()
            if (!(providerFactory instanceof ThreadLocalResteasyProviderFactory)) {
                if (ResteasyProviderFactory.peekInstance() == null
                        || !(ResteasyProviderFactory.peekInstance() instanceof ThreadLocalResteasyProviderFactory)) {

                    threadLocalProviderFactory = new ThreadLocalResteasyProviderFactory(providerFactory,
                            defaultExceptionManagerEnabled);
                    ResteasyProviderFactory.setInstance(threadLocalProviderFactory);
                } else {
                    ThreadLocalResteasyProviderFactory.push(providerFactory);
                }
            } else {
                ThreadLocalResteasyProviderFactory.push(providerFactory);
            }
        } else {
            ResteasyProviderFactory.setInstance(providerFactory);
        }
    }

    public void merge(ResteasyDeployment other) {
        scannedResourceClasses.addAll(other.getScannedResourceClasses());
        scannedProviderClasses.addAll(other.getScannedProviderClasses());
        scannedJndiComponentResources.addAll(other.getScannedJndiComponentResources());
        scannedResourceClassesWithBuilder.putAll(other.getScannedResourceClassesWithBuilder());

        jndiComponentResources.addAll(other.getJndiComponentResources());
        providerClasses.addAll(other.getProviderClasses());
        actualProviderClasses.addAll(other.getActualProviderClasses());
        providers.addAll(other.getProviders());

        disabledProviders.addAll(other.getDisabledProviderClasses());

        jndiResources.addAll(other.getJndiResources());
        resourceClasses.addAll(other.getResourceClasses());
        unwrappedExceptions.addAll(other.getUnwrappedExceptions());
        actualResourceClasses.addAll(other.getActualResourceClasses());
        resourceFactories.addAll(other.getResourceFactories());
        resources.addAll(other.getResources());

        mediaTypeMappings.putAll(other.getMediaTypeMappings());
        languageExtensions.putAll(other.getLanguageExtensions());

        defaultContextObjects.putAll(other.getDefaultContextObjects());
        constructedDefaultContextObjects.putAll(other.getConstructedDefaultContextObjects());
    }

    public static Application createApplication(String applicationClass, Dispatcher dispatcher,
            ResteasyProviderFactory providerFactory) {
        Class<?> clazz = null;
        try {
            clazz = Thread.currentThread().getContextClassLoader().loadClass(applicationClass);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        Application app = (Application) providerFactory.createProviderInstance(clazz);
        dispatcher.getDefaultContextObjects().put(Application.class, app);
        ResteasyContext.pushContext(Application.class, app);
        @SuppressWarnings("unchecked")
        final ApplicationDescription applicationDescription = ApplicationDescription.Builder.of(app)
                .type((Class<? extends Application>) clazz)
                .build();
        dispatcher.getDefaultContextObjects().put(ApplicationDescription.class, applicationDescription);
        ResteasyContext.pushContext(ApplicationDescription.class, applicationDescription);
        PropertyInjector propertyInjector = providerFactory.getInjectorFactory().createPropertyInjector(clazz, providerFactory);
        propertyInjector.inject(app, false);
        return app;
    }

    private static Object createFromInjectorFactory(String classname, ResteasyProviderFactory providerFactory) {
        Class<?> clazz = null;
        try {
            clazz = Thread.currentThread().getContextClassLoader().loadClass(classname);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        Object obj = providerFactory.injectedInstance(clazz);

        return obj;
    }

    public void registration() {
        boolean useScanning = registerApplication();

        if (useScanning && scannedProviderClasses != null) {
            for (String provider : scannedProviderClasses) {
                if (disabledProviders.contains(provider)) {
                    LogMessages.LOGGER.debugf("Skipping provider \"%s\" as it is marked as disabled.", provider);
                } else {
                    registerProvider(provider);
                }
            }
        }

        if (providerClasses != null) {
            for (String provider : providerClasses) {
                if (disabledProviders.contains(provider)) {
                    LogMessages.LOGGER.debugf("Skipping provider \"%s\" as it is marked as disabled.", provider);
                } else {
                    registerProvider(provider);
                }
            }
        }
        if (providers != null) {
            for (Object provider : providers) {
                if (disabledProviders.contains(provider.getClass().getName())) {
                    LogMessages.LOGGER.debugf("Skipping provider \"%s\" as it is marked as disabled.",
                            provider.getClass().getName());
                } else {
                    providerFactory.registerProviderInstance(provider);
                }
            }
        }

        if (actualProviderClasses != null) {
            for (Class<?> actualProviderClass : actualProviderClasses) {
                if (disabledProviders.contains(actualProviderClass.getName())) {
                    LogMessages.LOGGER.debugf("Skipping provider \"%s\" as it is marked as disabled.",
                            actualProviderClass.getName());
                } else {
                    providerFactory.registerProvider(actualProviderClass);
                }
            }
        }
        registerResources(useScanning);

    }

    protected void registerResources(boolean useScanning) {
        // All providers should be registered before resources because of interceptors.
        // interceptors must exist as they are applied only once when the resource is registered.

        if (useScanning && scannedJndiComponentResources != null) {
            for (String resource : scannedJndiComponentResources) {
                registerJndiComponentResource(resource);
            }
        }
        if (jndiComponentResources != null) {
            for (String resource : jndiComponentResources) {
                registerJndiComponentResource(resource);
            }
        }
        if (jndiResources != null) {
            for (String resource : jndiResources) {
                registry.addJndiResource(resource.trim());
            }
        }

        if (useScanning && scannedResourceClasses != null) {
            for (String resource : scannedResourceClasses) {
                Class<?> clazz = null;
                try {
                    clazz = Thread.currentThread().getContextClassLoader().loadClass(resource.trim());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
                registry.addPerRequestResource(clazz);
            }
        }

        if (useScanning && scannedResourceClassesWithBuilder != null) {
            for (Map.Entry<String, List<String>> entry : scannedResourceClassesWithBuilder.entrySet()) {
                Class<?> resourceBuilderClass;
                try {
                    resourceBuilderClass = Thread.currentThread().getContextClassLoader().loadClass(entry.getKey().trim());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                if (!ResourceBuilder.class.isAssignableFrom(resourceBuilderClass)) {
                    throw new IllegalArgumentException("Supplied class: " + resourceBuilderClass + "must be a subclass of "
                            + ResourceBuilder.class.getName());
                }

                ResourceBuilder resourceBuilder;
                try {
                    resourceBuilder = (ResourceBuilder) resourceBuilderClass.newInstance();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

                for (String resource : entry.getValue()) {
                    Class<?> resourceClass;
                    try {
                        resourceClass = Thread.currentThread().getContextClassLoader().loadClass(resource.trim());
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }

                    registry.addPerRequestResource(resourceClass, resourceBuilder);
                }
            }
        }

        if (resourceClasses != null) {
            for (String resource : resourceClasses) {
                Class<?> clazz = null;
                try {
                    clazz = Thread.currentThread().getContextClassLoader().loadClass(resource.trim());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
                registry.addPerRequestResource(clazz);
            }
        }

        if (resources != null) {
            for (Object obj : resources) {
                registry.addSingletonResource(obj);
            }
        }

        if (actualResourceClasses != null) {
            for (Class<?> actualResourceClass : actualResourceClasses) {
                registry.addPerRequestResource(actualResourceClass);
            }
        }

        if (resourceFactories != null) {
            for (ResourceFactory factory : resourceFactories) {
                registry.addResourceFactory(factory);
            }
        }
        registry.checkAmbiguousUri();
    }

    protected boolean registerApplication() {
        boolean useScanning = true;
        if (application != null) {
            dispatcher.getDefaultContextObjects().put(Application.class, application);
            ResteasyContext.getContextDataMap().put(Application.class, application);
            // Potentially done twice as the current way this works createApplication() and registerApplication() may
            // both be invoked. There is no guarantee of that though.
            final ApplicationDescription applicationDescription = ApplicationDescription.Builder.of(application)
                    .build();
            dispatcher.getDefaultContextObjects().put(ApplicationDescription.class, applicationDescription);
            ResteasyContext.pushContext(ApplicationDescription.class, applicationDescription);
            if (processApplication(application)) {
                // Application class registered something so don't use scanning data.  See JAX-RS spec for more detail.
                useScanning = false;
            }
            // Jakarta REST 3.1 section 4.1.2 requires Feature's and DynamicFeature's to use a service loader if the
            // following property was not set on the Application.
            if (isEnabled(application.getProperties(), "jakarta.ws.rs.loadServices")) {
                actualProviderClasses.addAll(loadServices(Feature.class));
                actualProviderClasses.addAll(loadServices(DynamicFeature.class));
            }
        }
        return useScanning;
    }

    private void registerJndiComponentResource(String resource) {
        String[] config = resource.trim().split(";");
        if (config.length < 3) {
            throw new RuntimeException(Messages.MESSAGES.jndiComponentResourceNotSetCorrectly());
        }
        String jndiName = config[0];
        Class<?> clazz = null;
        try {
            clazz = Thread.currentThread().getContextClassLoader().loadClass(config[1]);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(Messages.MESSAGES.couldNotFindClassJndi(config[1]), e);
        }
        boolean cacheRefrence = Boolean.valueOf(config[2].trim());
        JndiComponentResourceFactory factory = new JndiComponentResourceFactory(jndiName, clazz, cacheRefrence);
        getResourceFactories().add(factory);

    }

    public void stop() {
        if (asyncJobServiceEnabled) {
            ((AsynchronousDispatcher) dispatcher).stop();
        }

        ResteasyProviderFactory.clearInstanceIfEqual(threadLocalProviderFactory);
        ResteasyProviderFactory.clearInstanceIfEqual(providerFactory);
    }

    /**
     * @param config application
     * @return whether application class registered anything. i.e. whether scanning metadata should be used or not
     */
    private boolean processApplication(Application config) {
        LogMessages.LOGGER.deployingApplication(Application.class.getName(), config.getClass());
        boolean registered = false;
        Set<Class<?>> classes = config.getClasses();
        if (classes != null) {
            for (Class<?> clazz : classes) {
                if (GetRestful.isRootResource(clazz)) {
                    LogMessages.LOGGER.addingClassResource(clazz.getName(), config.getClass());
                    actualResourceClasses.add(clazz);
                    registered = true;
                } else {
                    LogMessages.LOGGER.addingProviderClass(clazz.getName(), config.getClass());
                    actualProviderClasses.add(clazz);
                    registered = true;
                }
            }
        }
        Set<Object> singletons = config.getSingletons();
        if (singletons != null) {
            for (Object obj : singletons) {
                if (GetRestful.isRootResource(obj.getClass())) {
                    if (actualResourceClasses.contains(obj.getClass())) {
                        LogMessages.LOGGER.singletonClassAlreadyDeployed("resource", obj.getClass().getName());
                    } else {
                        LogMessages.LOGGER.addingSingletonResource(obj.getClass().getName(), config.getClass());
                        resources.add(obj);
                        registered = true;
                    }
                } else {
                    if (actualProviderClasses.contains(obj.getClass())) {
                        LogMessages.LOGGER.singletonClassAlreadyDeployed("provider", obj.getClass().getName());
                    } else {
                        LogMessages.LOGGER.addingProviderSingleton(obj.getClass().getName(), config.getClass());
                        providers.add(obj);
                        registered = true;
                    }
                }
            }
        }
        final Map<String, Object> properties = config.getProperties();
        if (properties != null && !properties.isEmpty()) {
            Feature applicationPropertiesRegistrationFeature = new Feature() {
                @Override
                public boolean configure(FeatureContext featureContext) {
                    for (Map.Entry<String, Object> property : properties.entrySet()) {
                        featureContext = featureContext.property(property.getKey(), property.getValue());
                    }
                    return true;
                }
            };
            this.providers.add(0, applicationPropertiesRegistrationFeature);
        }
        return registered;
    }

    private void registerProvider(String clazz) {
        Class<?> provider = null;
        try {
            provider = Thread.currentThread().getContextClassLoader().loadClass(clazz.trim());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        providerFactory.registerProvider(provider);
    }

    public boolean isUseContainerFormParams() {
        return useContainerFormParams;
    }

    public void setUseContainerFormParams(boolean useContainerFormParams) {
        this.useContainerFormParams = useContainerFormParams;
    }

    public List<String> getJndiComponentResources() {
        return jndiComponentResources;
    }

    public void setJndiComponentResources(List<String> jndiComponentResources) {
        this.jndiComponentResources = jndiComponentResources;
    }

    public String getApplicationClass() {
        return applicationClass;
    }

    public void setApplicationClass(String applicationClass) {
        this.applicationClass = applicationClass;
    }

    public String getInjectorFactoryClass() {
        return injectorFactoryClass;
    }

    public void setInjectorFactoryClass(String injectorFactoryClass) {
        this.injectorFactoryClass = injectorFactoryClass;
    }

    public boolean isDeploymentSensitiveFactoryEnabled() {
        return deploymentSensitiveFactoryEnabled;
    }

    public void setDeploymentSensitiveFactoryEnabled(boolean deploymentSensitiveFactoryEnabled) {
        this.deploymentSensitiveFactoryEnabled = deploymentSensitiveFactoryEnabled;
    }

    public boolean isAsyncJobServiceEnabled() {
        return asyncJobServiceEnabled;
    }

    public void setAsyncJobServiceEnabled(boolean asyncJobServiceEnabled) {
        this.asyncJobServiceEnabled = asyncJobServiceEnabled;
    }

    public int getAsyncJobServiceMaxJobResults() {
        return asyncJobServiceMaxJobResults;
    }

    public void setAsyncJobServiceMaxJobResults(int asyncJobServiceMaxJobResults) {
        this.asyncJobServiceMaxJobResults = asyncJobServiceMaxJobResults;
    }

    public long getAsyncJobServiceMaxWait() {
        return asyncJobServiceMaxWait;
    }

    public void setAsyncJobServiceMaxWait(long asyncJobServiceMaxWait) {
        this.asyncJobServiceMaxWait = asyncJobServiceMaxWait;
    }

    public int getAsyncJobServiceThreadPoolSize() {
        return asyncJobServiceThreadPoolSize;
    }

    public void setAsyncJobServiceThreadPoolSize(int asyncJobServiceThreadPoolSize) {
        this.asyncJobServiceThreadPoolSize = asyncJobServiceThreadPoolSize;
    }

    public String getAsyncJobServiceBasePath() {
        return asyncJobServiceBasePath;
    }

    public void setAsyncJobServiceBasePath(String asyncJobServiceBasePath) {
        this.asyncJobServiceBasePath = asyncJobServiceBasePath;
    }

    public Application getApplication() {
        return application;
    }

    public void setApplication(Application application) {
        this.application = application;
    }

    public boolean isRegisterBuiltin() {
        return registerBuiltin;
    }

    public void setRegisterBuiltin(boolean registerBuiltin) {
        this.registerBuiltin = registerBuiltin;
    }

    public List<String> getProviderClasses() {
        return providerClasses;
    }

    public void setProviderClasses(List<String> providerClasses) {
        this.providerClasses = providerClasses;
    }

    public List<Object> getProviders() {
        return providers;
    }

    public void setProviders(List<Object> providers) {
        this.providers = providers;
    }

    @SuppressWarnings("rawtypes")
    public List<Class> getActualProviderClasses() {
        return actualProviderClasses;
    }

    @SuppressWarnings("rawtypes")
    public void setActualProviderClasses(List<Class> actualProviderClasses) {
        this.actualProviderClasses = actualProviderClasses;
    }

    @Override
    public Set<String> getDisabledProviderClasses() {
        return Set.copyOf(disabledProviders);
    }

    @Override
    public void addDisabledProviderClass(final String disabledProviderClass) {
        this.disabledProviders.add(disabledProviderClass);
    }

    @Override
    public void addDisabledProviderClasses(final Set<String> disabledProviderClasses) {
        this.disabledProviders.addAll(disabledProviderClasses);
    }

    @Override
    public void setDisabledProviderClasses(final Set<String> disabledProviderClasses) {
        disabledProviders.clear();
        disabledProviders.addAll(disabledProviderClasses);
    }

    @Override
    public void setDisabledProviderClasses(final String... disabledProviderClasses) {
        setDisabledProviderClasses(Set.of(disabledProviderClasses));
    }

    @SuppressWarnings("rawtypes")
    public List<Class> getActualResourceClasses() {
        return actualResourceClasses;
    }

    @SuppressWarnings("rawtypes")
    public void setActualResourceClasses(List<Class> actualResourceClasses) {
        this.actualResourceClasses = actualResourceClasses;
    }

    public boolean isSecurityEnabled() {
        return securityEnabled;
    }

    public void setSecurityEnabled(boolean securityEnabled) {
        this.securityEnabled = securityEnabled;
    }

    public List<String> getJndiResources() {
        return jndiResources;
    }

    public void setJndiResources(List<String> jndiResources) {
        this.jndiResources = jndiResources;
    }

    public List<String> getResourceClasses() {
        return resourceClasses;
    }

    public void setResourceClasses(List<String> resourceClasses) {
        this.resourceClasses = resourceClasses;
    }

    public Map<String, String> getMediaTypeMappings() {
        return mediaTypeMappings;
    }

    public void setMediaTypeMappings(Map<String, String> mediaTypeMappings) {
        this.mediaTypeMappings = mediaTypeMappings;
    }

    public List<Object> getResources() {
        return resources;
    }

    public void setResources(List<Object> resources) {
        this.resources = resources;
    }

    public Map<String, String> getLanguageExtensions() {
        return languageExtensions;
    }

    public void setLanguageExtensions(Map<String, String> languageExtensions) {
        this.languageExtensions = languageExtensions;
    }

    public Registry getRegistry() {
        return registry;
    }

    public void setRegistry(Registry registry) {
        this.registry = registry;
    }

    public Dispatcher getDispatcher() {
        return dispatcher;
    }

    public void setDispatcher(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public ResteasyProviderFactory getProviderFactory() {
        return providerFactory;
    }

    public void setProviderFactory(ResteasyProviderFactory providerFactory) {
        this.providerFactory = providerFactory;
    }

    public void setMediaTypeParamMapping(String paramMapping) {
        this.paramMapping = paramMapping;
    }

    public List<ResourceFactory> getResourceFactories() {
        return resourceFactories;
    }

    public void setResourceFactories(List<ResourceFactory> resourceFactories) {
        this.resourceFactories = resourceFactories;
    }

    public List<String> getUnwrappedExceptions() {
        return unwrappedExceptions;
    }

    public void setUnwrappedExceptions(List<String> unwrappedExceptions) {
        this.unwrappedExceptions = unwrappedExceptions;
    }

    public Map<String, String> getConstructedDefaultContextObjects() {
        return constructedDefaultContextObjects;
    }

    public void setConstructedDefaultContextObjects(Map<String, String> constructedDefaultContextObjects) {
        this.constructedDefaultContextObjects = constructedDefaultContextObjects;
    }

    @SuppressWarnings("rawtypes")
    public Map<Class, Object> getDefaultContextObjects() {
        return defaultContextObjects;
    }

    @SuppressWarnings("rawtypes")
    public void setDefaultContextObjects(Map<Class, Object> defaultContextObjects) {
        this.defaultContextObjects = defaultContextObjects;
    }

    public List<String> getScannedResourceClasses() {
        return scannedResourceClasses;
    }

    public void setScannedResourceClasses(List<String> scannedResourceClasses) {
        this.scannedResourceClasses = scannedResourceClasses;
    }

    public List<String> getScannedProviderClasses() {
        return scannedProviderClasses;
    }

    public void setScannedProviderClasses(List<String> scannedProviderClasses) {
        this.scannedProviderClasses = scannedProviderClasses;
    }

    public List<String> getScannedJndiComponentResources() {
        return scannedJndiComponentResources;
    }

    public void setScannedJndiComponentResources(List<String> scannedJndiComponentResources) {
        this.scannedJndiComponentResources = scannedJndiComponentResources;
    }

    @Override
    public Map<String, List<String>> getScannedResourceClassesWithBuilder() {
        return scannedResourceClassesWithBuilder;
    }

    @Override
    public void setScannedResourceClassesWithBuilder(Map<String, List<String>> scannedResourceClassesWithBuilder) {
        this.scannedResourceClassesWithBuilder = scannedResourceClassesWithBuilder;
    }

    public boolean isWiderRequestMatching() {
        return widerRequestMatching;
    }

    public void setWiderRequestMatching(boolean widerRequestMatching) {
        this.widerRequestMatching = widerRequestMatching;
    }

    public boolean isAddCharset() {
        return addCharset;
    }

    public void setAddCharset(boolean addCharset) {
        this.addCharset = addCharset;
    }

    public InjectorFactory getInjectorFactory() {
        return injectorFactory;
    }

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

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

    @Override
    public void setProperty(String key, Object value) {
        properties.put(key, value);
    }

    @Override
    public void setStatisticsEnabled(boolean statisticsEnabled) {
        this.statisticsEnabled = statisticsEnabled;
    }

    private static Set<Class<?>> loadServices(final Class<?> service) {
        final Set<Class<?>> results = new LinkedHashSet<>();
        results.addAll(PriorityServiceLoader.load(service).getTypes());
        results.addAll(PriorityServiceLoader.load(service, service.getClassLoader()).getTypes());
        return results;
    }

    private static boolean isEnabled(final Map<String, Object> properties, final String name) {
        final Object value = properties.get(name);
        if (value == null) {
            return true;
        }
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        return !String.valueOf(value).equalsIgnoreCase("false");
    }
}
