/*
 *     Licensed to the Apache Software Foundation (ASF) under one or more
 *     contributor license agreements.  See the NOTICE file distributed with
 *     this work for additional information regarding copyright ownership.
 *     The ASF licenses this file to You under the Apache License, Version 2.0
 *     (the "License"); you may not use this file except in compliance with
 *     the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 */
package org.apache.openejb.server.cxf.rs;

import org.apache.cxf.BusException;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.endpoint.EndpointException;
import org.apache.cxf.endpoint.ManagedEndpoint;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.endpoint.ServerImpl;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.JAXRSServiceImpl;
import org.apache.cxf.jaxrs.ext.ResourceComparator;
import org.apache.cxf.jaxrs.lifecycle.ResourceProvider;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.model.MethodDispatcher;
import org.apache.cxf.jaxrs.model.OperationResourceInfo;
import org.apache.cxf.jaxrs.model.ProviderInfo;
import org.apache.cxf.jaxrs.provider.ProviderFactory;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.jaxrs.validation.ValidationExceptionMapper;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.invoker.Invoker;
import org.apache.cxf.transport.DestinationFactory;
import org.apache.cxf.transport.servlet.BaseUrlHelper;
import org.apache.johnzon.jaxrs.ConfigurableJohnzonProvider;
import org.apache.johnzon.jaxrs.JohnzonProvider;
import org.apache.johnzon.jaxrs.WadlDocumentMessageBodyWriter;
import org.apache.openejb.AppContext;
import org.apache.openejb.BeanContext;
import org.apache.openejb.Injection;
import org.apache.openejb.api.internal.Internal;
import org.apache.openejb.api.jmx.Description;
import org.apache.openejb.api.jmx.MBean;
import org.apache.openejb.api.jmx.ManagedAttribute;
import org.apache.openejb.api.jmx.ManagedOperation;
import org.apache.openejb.assembler.classic.ServiceInfo;
import org.apache.openejb.assembler.classic.util.ServiceConfiguration;
import org.apache.openejb.assembler.classic.util.ServiceInfos;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.dyni.DynamicSubclass;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.monitoring.LocalMBeanServer;
import org.apache.openejb.monitoring.ObjectNameBuilder;
import org.apache.openejb.rest.ThreadLocalContextManager;
import org.apache.openejb.server.cxf.rs.event.ExtensionProviderRegistration;
import org.apache.openejb.server.cxf.rs.event.ServerCreated;
import org.apache.openejb.server.cxf.rs.event.ServerDestroyed;
import org.apache.openejb.server.cxf.transport.HttpDestination;
import org.apache.openejb.server.cxf.transport.util.CxfUtil;
import org.apache.openejb.server.httpd.HttpRequest;
import org.apache.openejb.server.httpd.HttpRequestImpl;
import org.apache.openejb.server.httpd.HttpResponse;
import org.apache.openejb.server.httpd.ServletRequestAdapter;
import org.apache.openejb.server.rest.EJBRestServiceInfo;
import org.apache.openejb.server.rest.InternalApplication;
import org.apache.openejb.server.rest.RsHttpListener;
import org.apache.openejb.util.AppFinder;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.proxy.ProxyEJB;
import org.apache.openejb.util.reflection.Reflections;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.webbeans.container.BeanManagerImpl;
import org.apache.webbeans.context.creational.CreationalContextImpl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.regex.Pattern;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.management.ObjectName;
import javax.management.openmbean.TabularData;
import javax.naming.Context;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.ConstrainedTo;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;

import static java.util.Arrays.asList;
import static org.apache.openejb.loader.JarLocation.jarLocation;

public class CxfRsHttpListener implements RsHttpListener {

    private static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB_RS, CxfRsHttpListener.class);

    private static final java.util.logging.Logger SERVER_IMPL_LOGGER = LogUtils.getL7dLogger(ServerImpl.class);

    public static final String CXF_JAXRS_PREFIX = "cxf.jaxrs.";
    public static final String PROVIDERS_KEY = CXF_JAXRS_PREFIX + "providers";
    public static final String STATIC_RESOURCE_KEY = CXF_JAXRS_PREFIX + "static-resources-list";
    public static final String STATIC_SUB_RESOURCE_RESOLUTION_KEY = "staticSubresourceResolution";
    public static final String RESOURCE_COMPARATOR_KEY = CXF_JAXRS_PREFIX + "resourceComparator";

    private static final String GLOBAL_PROVIDERS = SystemInstance.get().getProperty(PROVIDERS_KEY);
    public static final boolean TRY_STATIC_RESOURCES = "true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.jaxrs.static-first", "true"));
    private static final boolean FAIL_ON_CONSTRAINED_TO = "true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.jaxrs.fail-on-constrainedto", "true"));

    private static final Map<String, String> STATIC_CONTENT_TYPES;
    private static final String[] DEFAULT_WELCOME_FILES = new String[]{ "/index.html", "/index.htm" };

    private final DestinationFactory transportFactory;
    private final String wildcard;
    private HttpDestination destination;
    private Server server;
    private String context = "";
    private String servlet = "";
    private final Collection<Pattern> staticResourcesList = new CopyOnWriteArrayList<>();
    private final List<ObjectName> jmxNames = new ArrayList<>();
    private final Collection<CreationalContext<?>> toRelease = new LinkedHashSet<>();
    private final Collection<CdiSingletonResourceProvider> singletons = new LinkedHashSet<>();

    private static final char[] URL_SEP = new char[] { '?', '#', ';' };

    static {
        STATIC_CONTENT_TYPES = new HashMap<>();
        STATIC_CONTENT_TYPES.put("html", "text/html");
        STATIC_CONTENT_TYPES.put("htm", "text/html");
        STATIC_CONTENT_TYPES.put("xhtml", "text/html");
        STATIC_CONTENT_TYPES.put("txt", "text/plain");
        STATIC_CONTENT_TYPES.put("css", "text/css");
        STATIC_CONTENT_TYPES.put("jpg", "image/jpg");
        STATIC_CONTENT_TYPES.put("png", "image/png");
        STATIC_CONTENT_TYPES.put("ico", "image/ico");
        STATIC_CONTENT_TYPES.put("pdf", "application/pdf");
        STATIC_CONTENT_TYPES.put("xsd", "application/xml");
    }

    private String pattern;

    public CxfRsHttpListener(final DestinationFactory destinationFactory, final String star) {
        transportFactory = destinationFactory;
        wildcard = star;
    }

    public void setUrlPattern(final String pattern) {
        this.pattern = pattern;
    }

    @Override
    public void onMessage(final HttpRequest httpRequest, final HttpResponse httpResponse) throws Exception {
        // fix the address (to manage multiple connectors)
        if (HttpRequestImpl.class.isInstance(httpRequest)) {
            final HttpRequestImpl requestImpl = HttpRequestImpl.class.cast(httpRequest);
            requestImpl.initPathFromContext((!context.startsWith("/") ? "/" : "") + context);
            requestImpl.initServletPath(servlet);
        }

        boolean matchedStatic = false;
        if (TRY_STATIC_RESOURCES || (matchedStatic = matchPath(httpRequest))) {
            final String pathInfo = httpRequest.getPathInfo();
            if (serveStaticContent(httpRequest, httpResponse, pathInfo)) {
                if (matchedStatic) { // we should have gotten the resource
                    throw new ServletException("Static resource " + pathInfo + " is not available");
                }
                return; // ok that's a surely rest service
            }
        }

        doInvoke(httpRequest, httpResponse);
    }

    // normal endpoint without static resource handling
    public void doInvoke(final HttpRequest httpRequest, final HttpResponse httpResponse) throws IOException {
        String baseURL = BaseUrlHelper.getBaseURL(pattern != null ? new ServletRequestAdapter(httpRequest) {
            @Override // we have a filter so we need the computed servlet path to not break CXF
            public String getServletPath() {
                return pattern;
            }
        } : httpRequest);
        if (!baseURL.endsWith("/")) {
            baseURL += "/";
        }
        httpRequest.setAttribute("org.apache.cxf.transport.endpoint.address", baseURL);

        final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(CxfUtil.initBusLoader());
        try {
            destination.invoke(null, httpRequest.getServletContext(), httpRequest, httpResponse);
        } finally {
            CxfUtil.clearBusLoader(oldLoader);
        }
    }

    public boolean matchPath(final HttpServletRequest request) {
        if (staticResourcesList.isEmpty()) {
            return false;
        }

        String path = request.getRequestURI().substring(request.getContextPath().length());
        if (path.isEmpty()) {
            path = "/";
        }
        for (final Pattern pattern : staticResourcesList) {
            if (pattern.matcher(path).matches()) {
                return true;
            }
        }
        return false;
    }

    public InputStream findStaticContent(final HttpServletRequest request, final String[] welcomeFiles) throws ServletException {
        String pathInfo = request.getRequestURI().substring(request.getContextPath().length());
        for (final char c : URL_SEP) {
            final int indexOf = pathInfo.indexOf(c);
            if (indexOf > 0) {
                pathInfo = pathInfo.substring(0, indexOf);
            }
        }
        InputStream is = request.getServletContext().getResourceAsStream(pathInfo);
        if (is == null && ("/".equals(pathInfo) || pathInfo.isEmpty())) {
            for (final String n : welcomeFiles) {
                is = request.getServletContext().getResourceAsStream(n);
                if (is != null) {
                    break;
                }
            }
        }
        return is;
    }

    public boolean serveStaticContent(final HttpServletRequest request,
                                      final HttpServletResponse response,
                                      final String pathInfo) throws ServletException {
        final InputStream is = findStaticContent(request, DEFAULT_WELCOME_FILES);
        if (is == null) {
            return false;
        }
        try {
            final int ind = pathInfo.lastIndexOf(".");
            if (ind != -1 && ind < pathInfo.length()) {
                final String type = STATIC_CONTENT_TYPES.get(pathInfo.substring(ind + 1));
                if (type != null) {
                    response.setContentType(type);
                }
            }

            final ServletOutputStream os = response.getOutputStream();
            IOUtils.copy(is, os);
            os.flush();
            response.setStatus(HttpURLConnection.HTTP_OK);
        } catch (final IOException ex) {
            throw new ServletException("Static resource " + pathInfo + " can not be written to the output stream");
        }
        return true;
    }

    @Override
    @Deprecated // we could drop it now I think
    public void deploySingleton(final String contextRoot, final String fullContext, final Object o, final Application appInstance,
                                final Collection<Object> additionalProviders, final ServiceConfiguration configuration) {
        deploy(contextRoot, o.getClass(), fullContext, new SingletonResourceProvider(o), o, appInstance, null, additionalProviders, configuration, null);
    }

    @Override
    @Deprecated // we could drop it now I think
    public void deployPojo(final ClassLoader loader,
                           final String contextRoot,
                           final String fullContext,
                           final Class<?> loadedClazz,
                           final Application app,
                           final Collection<Injection> injections,
                           final Context context,
                           final WebBeansContext owbCtx,
                           final Collection<Object> additionalProviders,
                           final ServiceConfiguration configuration) {
        deploy(contextRoot, loadedClazz, fullContext, new OpenEJBPerRequestPojoResourceProvider(loader, loadedClazz, injections, context, owbCtx),
            null, app, null, additionalProviders, configuration, owbCtx);
    }

    @Override
    @Deprecated // we could drop it now I think
    public void deployEJB(final String contextRoot,
                          final String fullContext,
                          final BeanContext beanContext,
                          final Collection<Object> additionalProviders,
                          final ServiceConfiguration configuration) {
        final Object proxy = ProxyEJB.subclassProxy(beanContext);

        deploy(contextRoot, beanContext.getBeanClass(), fullContext, new NoopResourceProvider(beanContext.getBeanClass(), proxy),
            proxy, null, new OpenEJBEJBInvoker(Collections.singleton(beanContext)), additionalProviders, configuration,
            beanContext.getWebBeansContext());
    }

    private void deploy(final String contextRoot, final Class<?> clazz, final String address, final ResourceProvider rp, final Object serviceBean,
                        final Application app, final Invoker invoker, final Collection<Object> additionalProviders, final ServiceConfiguration configuration,
                        final WebBeansContext webBeansContext) {
        final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(CxfUtil.initBusLoader());
        try {
            final JAXRSServerFactoryBean factory = newFactory(address, createServiceJmxName(clazz.getClassLoader()), createEndpointName(app));
            configureFactory(additionalProviders, configuration, factory, webBeansContext);
            factory.setResourceClasses(clazz);
            context = contextRoot;
            if (context == null) {
                context = "";
            }
            if (!context.startsWith("/")) {
                context = "/" + context;
            }

            if (rp != null) {
                factory.setResourceProvider(rp);
            }
            if (app != null) {
                factory.setApplication(app);
            }
            if (invoker != null) {
                factory.setInvoker(invoker);
            }
            if (serviceBean != null) {
                factory.setServiceBean(serviceBean);
            } else {
                factory.setServiceClass(clazz);
            }

            server = factory.create();
            destination = (HttpDestination) server.getDestination();

            fireServerCreated(oldLoader);
        } finally {
            if (oldLoader != null) {
                CxfUtil.clearBusLoader(oldLoader);
            }
        }
    }

    private void fireServerCreated(final ClassLoader oldLoader) {
        final Object ctx = AppFinder.findAppContextOrWeb(oldLoader, new AppFinder.Transformer<Object>() {
            @Override
            public Object from(final AppContext appCtx) {
                return appCtx;
            }

            @Override
            public Object from(final WebContext webCtx) {
                return webCtx;
            }
        });
        final AppContext appCtx = AppContext.class.isInstance(ctx) ? AppContext.class.cast(ctx) : WebContext.class.cast(ctx).getAppContext();
        WebContext webContext = appCtx == ctx ? null : WebContext.class.cast(ctx);
        if (webContext == null && appCtx.getWebContexts().size() == 1 && appCtx.getWebContexts().get(0).getClassLoader() == oldLoader) {
            webContext = appCtx.getWebContexts().get(0);
        }
        SystemInstance.get().fireEvent(new ServerCreated(server, appCtx, webContext, this.context));
    }

    private List<Object> providers(final Collection<ServiceInfo> services, final Collection<Object> additionalProviders, final WebBeansContext ctx) {
        final List<Object> instances = new ArrayList<>();
        final BeanManagerImpl bm = ctx == null ? null : ctx.getBeanManagerImpl();
        for (final Object o : additionalProviders) {
            if (o instanceof Class<?>) {
                final Class<?> clazz = (Class<?>) o;
                if (isNotServerProvider(clazz)) {
                    continue;
                }

                final String name = clazz.getName();
                if (shouldSkipProvider(name)) {
                    continue;
                }

                if (bm != null && bm.isInUse()) {
                    try {
                        final Set<Bean<?>> beans = bm.getBeans(clazz);
                        if (beans != null && !beans.isEmpty()) {
                            final Bean<?> bean = bm.resolve(beans);
                            final CreationalContextImpl<?> creationalContext = bm.createCreationalContext(bean);
                            instances.add(bm.getReference(bean, clazz, creationalContext));
                            toRelease.add(creationalContext);
                            continue;
                        }
                    } catch (final Throwable th) {
                        LOGGER.info("Can't use CDI to create provider " + name);
                    }
                }

                final Collection<Object> instance = ServiceInfos.resolve(services, new String[]{name}, OpenEJBProviderFactory.INSTANCE);
                if (instance != null && !instance.isEmpty()) {
                    instances.add(instance.iterator().next());
                } else {
                    try {
                        instances.add(newProvider(clazz));
                    } catch (final Exception e) {
                        LOGGER.error("can't instantiate " + name, e);
                    }
                }
            } else {
                final Class<?> clazz = o.getClass();
                if (isNotServerProvider(clazz)) {
                    continue;
                }
                final String name = clazz.getName();
                if (shouldSkipProvider(name)) {
                    continue;
                }
                instances.add(o);
            }
        }

        return instances;
    }

    private boolean isNotServerProvider(Class<?> clazz) {
        final ConstrainedTo ct = clazz.getAnnotation(ConstrainedTo.class);
        if (ct != null && ct.value() != RuntimeType.SERVER) {
            if (!FAIL_ON_CONSTRAINED_TO) {
                LOGGER.warning(clazz + " is not a SERVER provider, ignoring");
                return true;
            }
            throw new IllegalArgumentException(clazz + " is not a SERVER provider");
        }
        return false;
    }

    private static boolean shouldSkipProvider(final String name) {
        return "false".equalsIgnoreCase(SystemInstance.get().getProperty(name + ".activated", "true"))
                || name.startsWith("org.apache.wink.common.internal.");
    }

    private static void addMandatoryProviders(final Collection<Object> instances) {
        if (!shouldSkipProvider(WadlDocumentMessageBodyWriter.class.getName())) {
            instances.add(new WadlDocumentMessageBodyWriter());
        }
        if (!shouldSkipProvider(EJBExceptionMapper.class.getName())) {
            instances.add(new EJBExceptionMapper());
        }
        if (!shouldSkipProvider(ValidationExceptionMapper.class.getName())) {
            instances.add(new ValidationExceptionMapper());
        }
    }

    private Object newProvider(final Class<?> clazz) throws IllegalAccessException, InstantiationException {
        return clazz.newInstance();
    }

    @Override
    public void undeploy() {
        for (final ObjectName objectName : jmxNames) {
            LocalMBeanServer.unregisterSilently(objectName);
        }

        for (final CreationalContext<?> cc : toRelease) {
            try {
                cc.release();
            } catch (final Exception e) {
                LOGGER.warning(e.getMessage(), e);
            }
        }
        for (final CdiSingletonResourceProvider provider : singletons) {
            try {
                provider.release();
            } catch (final Exception e) {
                LOGGER.warning(e.getMessage(), e);
            }
        }

        final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(CxfUtil.initBusLoader());
        try {
            server.destroy();
            SystemInstance.get().fireEvent(new ServerDestroyed(server));
        } catch (final RuntimeException ise) {
            LOGGER.warning("Can't stop correctly the endpoint " + server);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(ise.getMessage(), ise);
            }
        } finally {
            if (oldLoader != null) {
                CxfUtil.clearBusLoader(oldLoader);
            }
        }
    }

    @Override
    public void deployApplication(final Application application, final String prefix, final String webContext,
                                  final Collection<Object> additionalProviders,
                                  final Map<String, EJBRestServiceInfo> restEjbs, final ClassLoader classLoader,
                                  final Collection<Injection> injections, final Context context, final WebBeansContext owbCtx,
                                  final ServiceConfiguration serviceConfiguration) {
        final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(CxfUtil.initBusLoader());
        try {
            final JAXRSServerFactoryBean factory = newFactory(prefix, createServiceJmxName(classLoader), createEndpointName(application));
            configureFactory(additionalProviders, serviceConfiguration, factory, owbCtx);
            factory.setApplication(application);

            final List<Class<?>> classes = new ArrayList<>();

            for (final Class<?> clazz : application.getClasses()) {
                if (!additionalProviders.contains(clazz) && !clazz.isInterface()) {
                    classes.add(clazz);

                    final EJBRestServiceInfo restServiceInfo = getEjbRestServiceInfo(restEjbs, clazz);

                    if (restServiceInfo != null) {
                        final Object proxy = ProxyEJB.subclassProxy(restServiceInfo.context);
                        factory.setResourceProvider(clazz, new NoopResourceProvider(restServiceInfo.context.getBeanClass(), proxy));
                    } else {
                        factory.setResourceProvider(clazz, new OpenEJBPerRequestPojoResourceProvider(
                                classLoader, clazz, injections, context, owbCtx));
                    }
                }
            }

            for (final Object o : application.getSingletons()) {
                if (!additionalProviders.contains(o)) {
                    final Class<?> clazz = realClass(o.getClass());
                    classes.add(clazz);

                    final EJBRestServiceInfo restServiceInfo = getEjbRestServiceInfo(restEjbs, clazz);

                    if (restServiceInfo != null) {
                        final Object proxy = ProxyEJB.subclassProxy(restServiceInfo.context);
                        factory.setResourceProvider(clazz, new NoopResourceProvider(restServiceInfo.context.getBeanClass(), proxy));
                    } else {
                        if (owbCtx.getBeanManagerImpl().isInUse()) {
                            final CdiSingletonResourceProvider provider = new CdiSingletonResourceProvider(classLoader, clazz, o, injections, context, owbCtx);
                            singletons.add(provider);
                            factory.setResourceProvider(clazz, provider);
                        } else {
                            factory.setResourceProvider(clazz, new SingletonResourceProvider(o));
                        }
                    }
                }
            }

            factory.setResourceClasses(classes);
            factory.setInvoker(new AutoJAXRSInvoker(restEjbs));

            this.context = webContext;
            if (!webContext.startsWith("/")) {
                this.context = "/" + webContext;
            } // /webcontext/servlet for event firing

            final Level level = SERVER_IMPL_LOGGER.getLevel();
            try {
                SERVER_IMPL_LOGGER.setLevel(Level.OFF);
            } catch (final UnsupportedOperationException e) {
                //ignore
            }

            try {
                server = factory.create();
                fixProviderIfKnown();
                fireServerCreated(oldLoader);

                final ServerProviderFactory spf = ServerProviderFactory.class.cast(server.getEndpoint().get(ServerProviderFactory.class.getName()));
                LOGGER.info("Using readers:");
                for (final Object provider : List.class.cast(Reflections.get(spf, "messageReaders"))) {
                    LOGGER.info("     " + ProviderInfo.class.cast(provider).getProvider());
                }
                LOGGER.info("Using writers:");
                for (final Object provider : List.class.cast(Reflections.get(spf, "messageWriters"))) {
                    LOGGER.info("     " + ProviderInfo.class.cast(provider).getProvider());
                }
                LOGGER.info("Using exception mappers:");
                for (final Object provider : List.class.cast(Reflections.get(spf, "exceptionMappers"))) {
                    LOGGER.info("     " + ProviderInfo.class.cast(provider).getProvider());
                }
            } finally {
                try {
                    SERVER_IMPL_LOGGER.setLevel(level);
                } catch (final UnsupportedOperationException e) {
                    //ignore
                }
            }

            final int servletIdx = 1 + this.context.substring(1).indexOf('/');
            if (servletIdx > 0) {
                this.servlet = this.context.substring(servletIdx);
                this.context = this.context.substring(0, servletIdx);
            }
            destination = (HttpDestination) server.getDestination();

            final String base;
            if (prefix.endsWith("/")) {
                base = prefix.substring(0, prefix.length() - 1);
            } else if (prefix.endsWith(wildcard)) {
                base = prefix.substring(0, prefix.length() - wildcard.length());
            } else {
                base = prefix;
            }

            // stack info to log to get nice logs
            logEndpoints(application, prefix, restEjbs, factory, base);
        } finally {
            if (oldLoader != null) {
                CxfUtil.clearBusLoader(oldLoader);
            }
        }
    }

    private void fixProviderIfKnown() {
        final ServerProviderFactory spf = ServerProviderFactory.class.cast(server.getEndpoint().get(ServerProviderFactory.class.getName()));
        for (final String field : asList("messageWriters", "messageReaders")) {
            final List<ProviderInfo<?>> values = List.class.cast(Reflections.get(spf, field));

            boolean customJsonProvider = false;
            for (final ProviderInfo<?> o : values) { // using getName to not suppose any classloader setup
                if (ConfigurableJohnzonProvider.class.getName().equals(o.getResourceClass().getName())
                    // contains in case of proxying
                    || o.getResourceClass().getName().contains("com.fasterxml.jackson.jaxrs.json")) {
                    customJsonProvider = true;
                    break; //  cause we only handle json for now
                }
            }

            if (customJsonProvider) {
                final Iterator<ProviderInfo<?>> it = values.iterator();
                while (it.hasNext()) {
                    if (JohnzonProvider.class.getName().equals(it.next().getResourceClass().getName())) {
                        it.remove();
                        break;
                    }
                }
            }
        }

    }

    private static Class<?> realClass(final Class<?> aClass) {
        Class<?> result = aClass;
        while (result.getName().contains("$$")) {
            result = result.getSuperclass();
            if (result == null) {
                return aClass;
            }
        }
        return result;
    }

    private EJBRestServiceInfo getEjbRestServiceInfo(Map<String, EJBRestServiceInfo> restEjbs, Class<?> clazz) {
        String name = clazz.getName();
        EJBRestServiceInfo restServiceInfo = restEjbs.get(name);

        if (name.endsWith(DynamicSubclass.IMPL_SUFFIX)) {
            name = name.substring(0, name.length() - DynamicSubclass.IMPL_SUFFIX.length());
            restServiceInfo = restEjbs.get(name);
            if (restServiceInfo != null) { // AutoJAXRSInvoker relies on it
                restEjbs.put(clazz.getName(), restServiceInfo);
            }
        }
        return restServiceInfo;
    }

    private static String createEndpointName(final Application application) {
        if (application == null) {
            return "jaxrs-application";
        }
        if (InternalApplication.class.isInstance(application)) {
            final Application original = InternalApplication.class.cast(application).getOriginal();
            if (original != null) {
                return original.getClass().getSimpleName();
            }
            return "jaxrs-application";
        }
        return application.getClass().getSimpleName();
    }

    private static String createServiceJmxName(final ClassLoader classLoader) {
        final AppContext app = AppFinder.findAppContextOrWeb(classLoader, AppFinder.AppContextTransformer.INSTANCE);
        return app == null ? "application" : app.getId();
    }

    private void logEndpoints(final Application application, final String prefix,
                              final Map<String, EJBRestServiceInfo> restEjbs,
                              final JAXRSServerFactoryBean factory, final String base) {
        final List<Logs.LogResourceEndpointInfo> resourcesToLog = new ArrayList<>();
        int classSize = 0;
        int addressSize = 0;

        final JAXRSServiceImpl service = (JAXRSServiceImpl) factory.getServiceFactory().getService();
        final List<ClassResourceInfo> resources = service.getClassResourceInfos();
        for (final ClassResourceInfo info : resources) {
            if (info.getResourceClass() == null) { // possible?
                continue;
            }

            final String address = Logs.singleSlash(base, info.getURITemplate().getValue());

            final String clazz = info.getResourceClass().getName();
            final String type;
            if (restEjbs.containsKey(clazz)) {
                type = "EJB";
            } else {
                type = "Pojo";
            }

            classSize = Math.max(classSize, clazz.length());
            addressSize = Math.max(addressSize, address.length());

            int methodSize = 7;
            int methodStrSize = 0;

            final List<Logs.LogOperationEndpointInfo> toLog = new ArrayList<>();

            final MethodDispatcher md = info.getMethodDispatcher();
            for (final OperationResourceInfo ori : md.getOperationResourceInfos()) {
                final String httpMethod = ori.getHttpMethod();
                final String currentAddress = Logs.singleSlash(address, ori.getURITemplate().getValue());
                final String methodToStr = Logs.toSimpleString(ori.getMethodToInvoke());
                toLog.add(new Logs.LogOperationEndpointInfo(httpMethod, currentAddress, methodToStr));

                if (httpMethod != null) {
                    methodSize = Math.max(methodSize, httpMethod.length());
                }
                addressSize = Math.max(addressSize, currentAddress.length());
                methodStrSize = Math.max(methodStrSize, methodToStr.length());
            }

            Collections.sort(toLog);

            resourcesToLog.add(new Logs.LogResourceEndpointInfo(type, address, clazz, toLog, methodSize, methodStrSize));
        }

        // effective logging

        LOGGER.info("REST Application: " + Logs.forceLength(prefix, addressSize, true) + " -> " + application.getClass().getName());

        Collections.sort(resourcesToLog);

        for (final Logs.LogResourceEndpointInfo resource : resourcesToLog) {

            // Init and register MBeans
            final ObjectNameBuilder jmxName = new ObjectNameBuilder("openejb.management")
                .set("j2eeType", "JAX-RS")
                .set("J2EEServer", "openejb")
                .set("J2EEApplication", base)
                .set("EndpointType", resource.type)
                .set("name", resource.classname);

            final ObjectName jmxObjectName = jmxName.build();
            LocalMBeanServer.registerDynamicWrapperSilently(
                new RestServiceMBean(resource),
                jmxObjectName);

            jmxNames.add(jmxObjectName);

            LOGGER.info("     Service URI: "
                + Logs.forceLength(resource.address, addressSize, true) + " -> "
                + Logs.forceLength(resource.type, 4, false) + " "
                + Logs.forceLength(resource.classname, classSize, true));

            for (final Logs.LogOperationEndpointInfo log : resource.operations) {
                LOGGER.info("          "
                    + Logs.forceLength(log.http, resource.methodSize, false) + " "
                    + Logs.forceLength(log.address, addressSize, true) + " ->      "
                    + Logs.forceLength(log.method, resource.methodStrSize, true));
            }

            resource.operations.clear();
        }
        resourcesToLog.clear();
    }

    private JAXRSServerFactoryBean newFactory(final String prefix, final String service, final String endpoint) {
        final JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean() {
            @Override
            protected Endpoint createEndpoint() throws BusException, EndpointException {
                final Endpoint created = super.createEndpoint();
                created.put(ManagedEndpoint.SERVICE_NAME, service);
                created.put(ManagedEndpoint.ENDPOINT_NAME, endpoint);
                return created;
            }
        };
        factory.setDestinationFactory(transportFactory);
        factory.setBus(CxfUtil.getBus());
        factory.setAddress(prefix);
        return factory;
    }

    private void configureFactory(final Collection<Object> givenAdditionalProviders,
                                  final ServiceConfiguration serviceConfiguration,
                                  final JAXRSServerFactoryBean factory,
                                  final WebBeansContext ctx) {
        if (!"true".equalsIgnoreCase(SystemInstance.get().getProperty("openejb.cxf.rs.skip-provider-sorting", "false"))) {
            final Comparator<?> providerComparator = findProviderComparator(serviceConfiguration, ctx);
            if (providerComparator != null) {
                factory.setProviderComparator(providerComparator);
            }
        }
        CxfUtil.configureEndpoint(factory, serviceConfiguration, CXF_JAXRS_PREFIX);

        final Collection<ServiceInfo> services = serviceConfiguration.getAvailableServices();

        final String staticSubresourceResolution = serviceConfiguration.getProperties().getProperty(CXF_JAXRS_PREFIX + STATIC_SUB_RESOURCE_RESOLUTION_KEY);
        if (staticSubresourceResolution != null) {
            factory.setStaticSubresourceResolution("true".equalsIgnoreCase(staticSubresourceResolution));
        }

        // resource comparator
        final String resourceComparator = serviceConfiguration.getProperties().getProperty(RESOURCE_COMPARATOR_KEY);
        if (resourceComparator != null) {
            try {
                ResourceComparator instance = (ResourceComparator) ServiceInfos.resolve(services, resourceComparator);
                if (instance == null) {
                    instance = (ResourceComparator) Thread.currentThread().getContextClassLoader()
                        .loadClass(resourceComparator).newInstance();
                }
                factory.setResourceComparator(instance);
            } catch (final Exception e) {
                LOGGER.error("Can't create the resource comparator " + resourceComparator, e);
            }
        }

        // static resources
        final String staticResources = serviceConfiguration.getProperties().getProperty(STATIC_RESOURCE_KEY);
        if (staticResources != null) {
            final String[] resources = staticResources.split(",");
            for (final String r : resources) {
                final String trimmed = r.trim();
                if (!trimmed.isEmpty()) {
                    staticResourcesList.add(Pattern.compile(trimmed));
                }
            }
        }

        // providers
        Set<String> providersConfig = null;

        {
            final String provider = serviceConfiguration.getProperties().getProperty(PROVIDERS_KEY);
            if (provider != null) {
                providersConfig = new HashSet<>();
                for (final String p : Arrays.asList(provider.split(","))) {
                    providersConfig.add(p.trim());
                }
            }

            {
                if (GLOBAL_PROVIDERS != null) {
                    if (providersConfig == null) {
                        providersConfig = new HashSet<>();
                    }
                    providersConfig.addAll(Arrays.asList(GLOBAL_PROVIDERS.split(",")));
                }
            }
        }

        // another property to configure the scanning of providers but this one is consistent with current cxf config
        // the other one is more generic but need another file
        final String key = CXF_JAXRS_PREFIX + "skip-provider-scanning";
        final boolean ignoreAutoProviders = "true".equalsIgnoreCase(SystemInstance.get().getProperty(key, serviceConfiguration.getProperties().getProperty(key, "false")));
        final Collection<Object> additionalProviders = ignoreAutoProviders ? Collections.emptyList() : givenAdditionalProviders;
        List<Object> providers = null;
        if (providersConfig != null) {
            providers = ServiceInfos.resolve(services, providersConfig.toArray(new String[providersConfig.size()]), OpenEJBProviderFactory.INSTANCE);
            if (providers != null && additionalProviders != null && !additionalProviders.isEmpty()) {
                providers.addAll(providers(serviceConfiguration.getAvailableServices(), additionalProviders, ctx));
            }
        }
        if (providers == null) {
            providers = new ArrayList<>(4);
            if (additionalProviders != null && !additionalProviders.isEmpty()) {
                providers.addAll(providers(serviceConfiguration.getAvailableServices(), additionalProviders, ctx));
            }
        }

        if (!ignoreAutoProviders) {
            addMandatoryProviders(providers);
        }

        SystemInstance.get().fireEvent(new ExtensionProviderRegistration(
                AppFinder.findAppContextOrWeb(Thread.currentThread().getContextClassLoader(), AppFinder.AppContextTransformer.INSTANCE), providers));

        if (!providers.isEmpty()) {
            factory.setProviders(providers);
        }
    }

    private Comparator<?> findProviderComparator(final ServiceConfiguration serviceConfiguration, final WebBeansContext ctx) {
        final String comparatorKey = CXF_JAXRS_PREFIX + "provider-comparator";
        final String comparatorClass = serviceConfiguration.getProperties()
                                           .getProperty(comparatorKey, SystemInstance.get().getProperty(comparatorKey));

        Comparator<Object> comparator = null;
        if (comparatorClass == null) {
            return null; // try to rely on CXF behavior otherwise just reactivate DefaultProviderComparator.INSTANCE if it is an issue
        } else {
            final BeanManagerImpl bm = ctx == null ? null : ctx.getBeanManagerImpl();
            if (bm != null && bm.isInUse()) {
                try {
                    final Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(comparatorClass);
                    final Set<Bean<?>> beans = bm.getBeans(clazz);
                    if (beans != null && !beans.isEmpty()) {
                        final Bean<?> bean = bm.resolve(beans);
                        final CreationalContextImpl<?> creationalContext = bm.createCreationalContext(bean);
                        comparator = Comparator.class.cast(bm.getReference(bean, clazz, creationalContext));
                        toRelease.add(creationalContext);
                    }
                } catch (final Throwable th) {
                    LOGGER.debug("Can't use CDI to load comparator " + comparatorClass);
                }
            }

            if (comparator == null) {
                comparator = Comparator.class.cast(ServiceInfos.resolve(serviceConfiguration.getAvailableServices(), comparatorClass));
            }
            if (comparator == null) {
                try {
                    comparator = Comparator.class.cast(Thread.currentThread().getContextClassLoader().loadClass(comparatorClass).newInstance());
                } catch (final Exception e) {
                    throw new IllegalArgumentException(e);
                }
            }

            for (final Type itf : comparator.getClass().getGenericInterfaces()) {
                if (!ParameterizedType.class.isInstance(itf)) {
                    continue;
                }

                final ParameterizedType pt = ParameterizedType.class.cast(itf);
                if (Comparator.class == pt.getRawType() && pt.getActualTypeArguments().length > 0) {
                    final Type t = pt.getActualTypeArguments()[0];
                    if (Class.class.isInstance(t) && ProviderInfo.class == t) {
                        return comparator;
                    }
                    if (ParameterizedType.class.isInstance(t) && ProviderInfo.class == ParameterizedType.class.cast(t).getRawType()) {
                        return comparator;
                    }
                }
            }

            return new ProviderComparatorWrapper(comparator);
        }
    }

    // public to ensure it can be configured since not setup by default anymore
    public static final class DefaultProviderComparator extends ProviderFactory implements Comparator<ProviderInfo<?>> {
        private static final ClassLoader SYSTEM_LOADER = ClassLoader.getSystemClassLoader();

        public DefaultProviderComparator() {
            super(null);
        }

        @Override
        public int compare(final ProviderInfo<?> o1, final ProviderInfo<?> o2) {
            if (o1 == o2 || (o1 != null && o1.equals(o2))) {
                return 0;
            }
            if (o1 == null) {
                return -1;
            }
            if (o2 == null) {
                return 1;
            }

            final Class<?> c1 = o1.getProvider().getClass();
            final Class<?> c2 = o2.getProvider().getClass();
            if (c1.getName().startsWith("org.apache.cxf.")) {
                if (!c2.getName().startsWith("org.apache.cxf.")) {
                    return 1;
                }
                return -1;
            }
            if (c2.getName().startsWith("org.apache.cxf.")) {
                return -1;
            }

            final ClassLoader classLoader1 = c1.getClassLoader();
            final ClassLoader classLoader2 = c2.getClassLoader();

            final boolean loadersNotNull = classLoader1 != null && classLoader2 != null;

            if (classLoader1 != classLoader2
                    && loadersNotNull
                    && !classLoader1.equals(classLoader2) && !classLoader2.equals(classLoader1)) {
                if (isParent(classLoader1, classLoader2)) {
                    return 1;
                }
                if (isParent(classLoader2, classLoader1)) {
                    return -1;
                }
            } else {
                int result = compareClasses(o1.getProvider(), o2.getProvider());
                if (result != 0) {
                    return result;
                }

                if (MessageBodyWriter.class.isInstance(o1.getProvider())) {
                    final List<MediaType> types1 =
                            JAXRSUtils.sortMediaTypes(JAXRSUtils.getProviderProduceTypes(MessageBodyWriter.class.cast(o1.getProvider())), JAXRSUtils.MEDIA_TYPE_QS_PARAM);
                    final List<MediaType> types2 =
                            JAXRSUtils.sortMediaTypes(JAXRSUtils.getProviderProduceTypes(MessageBodyWriter.class.cast(o2.getProvider())), JAXRSUtils.MEDIA_TYPE_QS_PARAM);

                    if (types1.contains(MediaType.WILDCARD_TYPE) && !types2.contains(MediaType.WILDCARD_TYPE)) {
                        return 1;
                    }
                    if (types2.contains(MediaType.WILDCARD_TYPE) && !types1.contains(MediaType.WILDCARD_TYPE)) {
                        return -1;
                    }

                    result = JAXRSUtils.compareSortedMediaTypes(types1, types2, JAXRSUtils.MEDIA_TYPE_QS_PARAM);
                    if (result != 0) {
                        return result;
                    }
                } else if (MessageBodyReader.class.isInstance(o1.getProvider())) { // else is not super good but using both is not sa well so let it be for now
                    final List<MediaType> types1 =
                            JAXRSUtils.sortMediaTypes(JAXRSUtils.getProviderConsumeTypes(MessageBodyReader.class.cast(o1.getProvider())), null);
                    final List<MediaType> types2 =
                            JAXRSUtils.sortMediaTypes(JAXRSUtils.getProviderConsumeTypes(MessageBodyReader.class.cast(o2.getProvider())), null);

                    if (types1.contains(MediaType.WILDCARD_TYPE) && !types2.contains(MediaType.WILDCARD_TYPE)) {
                        return 1;
                    }
                    if (types2.contains(MediaType.WILDCARD_TYPE) && !types1.contains(MediaType.WILDCARD_TYPE)) {
                        return -1;
                    }

                    result = JAXRSUtils.compareSortedMediaTypes(types1, types2, JAXRSUtils.MEDIA_TYPE_QS_PARAM);
                    if (result != 0) {
                        return result;
                    }
                }

                final Boolean custom1 = o1.isCustom();
                final Boolean custom2 = o2.isCustom();
                final int customComp = custom1.compareTo(custom2) * -1;
                if (customComp != 0) {
                    return customComp;
                }

                try { // WEB-INF/classes will be before WEB-INF/lib
                    final File file1 = jarLocation(c1);
                    final File file2 = jarLocation(c2);
                    if ("classes".equals(file1.getName())) {
                        if ("classes".equals(file2.getName())) {
                            return c1.getName().compareTo(c2.getName());
                        }
                        return -1;
                    }
                    if ("classes".equals(file2.getName())) {
                        return 1;
                    }
                } catch (final Exception e) {
                    // no-op: sort by class name
                }
            }
            return c1.getName().compareTo(c2.getName());
        }

        private static boolean isParent(final ClassLoader l1, ClassLoader l2) {
            ClassLoader current = l2;
            while (current != null && current != SYSTEM_LOADER) {
                if (current.equals(l1) || l1.equals(current)) {
                    return true;
                }
                current = current.getParent();
            }
            return false;
        }

        @Override
        public Configuration getConfiguration(final Message message) {
            throw new UnsupportedOperationException("not a real inheritance");
        }

        @Override
        protected void setProviders(final boolean custom, final boolean bus, final Object... providers) {
            throw new UnsupportedOperationException("not a real inheritance");
        }
    }

    // we use Object cause an app with a custom comparator can desire to compare instances
    private static final class ProviderComparatorWrapper implements Comparator<ProviderInfo<?>> {
        private final Comparator<Object> delegate;

        private ProviderComparatorWrapper(final Comparator<Object> delegate) {
            this.delegate = delegate;
        }

        @Override
        public int compare(final ProviderInfo<?> o1, final ProviderInfo<?> o2) {
            return delegate.compare(o1.getProvider(), o2.getProvider());
        }
    }

    private static class OpenEJBProviderFactory implements ServiceInfos.Factory {
        private static final ServiceInfos.Factory INSTANCE = new OpenEJBProviderFactory();

        @Override
        public Object newInstance(final Class<?> clazz) throws Exception {
            boolean found = false;
            Object instance = null;
            for (final Constructor<?> c : clazz.getConstructors()) {
                int contextAnnotations = 0;
                for (final Annotation[] annotations : c.getParameterAnnotations()) {
                    for (final Annotation a : annotations) {
                        if (javax.ws.rs.core.Context.class.equals(a.annotationType())) {
                            contextAnnotations++;
                            break;
                        }
                    }
                }
                if (contextAnnotations == c.getParameterTypes().length) {
                    if (found) {
                        LOGGER.warning("Found multiple matching constructor for " + clazz.getName());
                        return instance;
                    }

                    final Object[] params = new Object[c.getParameterTypes().length];
                    for (int i = 0; i < params.length; i++) {
                        params[i] = ThreadLocalContextManager.findThreadLocal(c.getParameterTypes()[i]);
                        // params[i] can be null if not a known type
                    }
                    instance = c.newInstance(params);
                    found = true;
                }
            }
            if (instance != null) {
                return instance;
            }
            return clazz.newInstance();
        }
    }

    @SuppressWarnings("UnusedDeclaration")
    @MBean
    @Internal
    @Description("JAX-RS service information")
    public class RestServiceMBean {

        private final String type;
        private final String address;
        private final String classname;
        private final TabularData operations;

        public RestServiceMBean(final Logs.LogResourceEndpointInfo jmxName) {
            this.type = jmxName.type;
            this.address = jmxName.address;
            this.classname = jmxName.classname;

            final String[] names = new String[jmxName.operations.size()];
            final String[] values = new String[jmxName.operations.size()];
            int idx = 0;
            for (final Logs.LogOperationEndpointInfo operation : jmxName.operations) {
                names[idx] = operation.http + " " + operation.address;
                values[idx] = operation.method;
                idx++;
            }
            operations = LocalMBeanServer.tabularData("Operations", "Operations for this endpoint", names, values);
        }

        @ManagedAttribute
        @Description("The type of the REST service")
        public String getWadlUrl() {
            if (address.endsWith("?_wadl")) {
                return address;
            }
            return address + "?_wadl";
        }

        @ManagedOperation
        @Description("The type of the REST service")
        public String getWadl(final String format) {
            if (format != null && format.toLowerCase().contains("json")) {
                InputStream inputStream = null;
                try {
                    final URL url = new URL(getWadlUrl() + "&_type=json");
                    final HttpURLConnection connection = HttpURLConnection.class.cast(url.openConnection());
                    connection.setRequestProperty("Accept", "application/json");
                    connection.setRequestProperty("Content-type", "application/json");
                    inputStream = connection.getInputStream();
                    return IO.slurp(inputStream);
                } catch (final Exception e) {
                    return e.getMessage();
                } finally {
                    IO.close(inputStream);
                }
            } else { // xml
                try {
                    return IO.slurp(new URL(getWadlUrl()));
                } catch (final IOException e) {
                    return e.getMessage();
                }
            }
        }

        @ManagedAttribute
        @Description("The type of the REST service")
        public String getType() {
            return type;
        }

        @ManagedAttribute
        @Description("The REST service address")
        public String getAddress() {
            return address;
        }

        @ManagedAttribute
        @Description("The REST service class name")
        public String getClassname() {
            return classname;
        }

        @ManagedAttribute
        @Description("All available methods")
        public TabularData getOperations() {
            return operations;
        }
    }
}
