/*
 * 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.rest;

import org.apache.openejb.AppContext;
import org.apache.openejb.BeanContext;
import org.apache.openejb.BeanType;
import org.apache.openejb.Injection;
import org.apache.openejb.assembler.classic.AppInfo;
import org.apache.openejb.assembler.classic.Assembler;
import org.apache.openejb.assembler.classic.EjbJarInfo;
import org.apache.openejb.assembler.classic.EnterpriseBeanInfo;
import org.apache.openejb.assembler.classic.IdPropertiesInfo;
import org.apache.openejb.assembler.classic.ParamValueInfo;
import org.apache.openejb.assembler.classic.ServiceInfo;
import org.apache.openejb.assembler.classic.ServletInfo;
import org.apache.openejb.assembler.classic.WebAppInfo;
import org.apache.openejb.assembler.classic.event.AssemblerAfterApplicationCreated;
import org.apache.openejb.assembler.classic.event.AssemblerBeforeApplicationDestroyed;
import org.apache.openejb.assembler.classic.util.PojoUtil;
import org.apache.openejb.assembler.classic.util.ServiceConfiguration;
import org.apache.openejb.core.CoreContainerSystem;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.observer.Observes;
import org.apache.openejb.server.SelfManaging;
import org.apache.openejb.server.ServerService;
import org.apache.openejb.server.ServiceException;
import org.apache.openejb.server.ServiceManager;
import org.apache.openejb.server.httpd.BasicAuthHttpListenerWrapper;
import org.apache.openejb.server.httpd.HttpListener;
import org.apache.openejb.server.httpd.HttpListenerRegistry;
import org.apache.openejb.spi.ContainerSystem;
import org.apache.openejb.testing.rest.ContextProvider;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.xbean.finder.MetaAnnotatedClass;

import javax.naming.Context;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.Path;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.WriterInterceptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

@SuppressWarnings("UnusedDeclaration")
public abstract class RESTService implements ServerService, SelfManaging {

    public static final Logger LOGGER = Logger.getInstance(LogCategory.OPENEJB_RS, RESTService.class);
    private static final boolean OLD_WEBSERVICE_DEPLOYMENT = SystemInstance.get().getOptions().get("openejb.webservice.old-deployment", false);
    public static final String OPENEJB_USE_APPLICATION_PROPERTY = "openejb.jaxrs.application";
    private static final String APPLICATION_DEPLOYMENT = SystemInstance.get().getOptions().get(OPENEJB_USE_APPLICATION_PROPERTY, "true");
    public static final String OPENEJB_JAXRS_PROVIDERS_AUTO_PROP = "openejb.jaxrs.providers.auto";

    private static final String IP = "n/a";
    private static final int PORT = -1;
    public static final String NOPATH_PREFIX = "http://nopath/";

    private final Set<AppInfo> deployedApplications = new HashSet<>();
    private final Set<WebAppInfo> deployedWebApps = new HashSet<>();
    private Assembler assembler;
    private CoreContainerSystem containerSystem;
    private RsRegistry rsRegistry;
    private final List<DeployedService> services = new ArrayList<>();
    private String virtualHost = "localhost";
    private String auth = "NONE";
    private String realm = "PropertiesLogin";
    protected boolean enabled = true;
    private final String wildcard = SystemInstance.get().getProperty("openejb.rest.wildcard", ".*"); // embedded = regex, tomee = servlet

    public void afterApplicationCreated(final AppInfo appInfo, final WebAppInfo webApp) {
        final WebContext webContext = containerSystem.getWebContextByHost(webApp.moduleId, webApp.host != null ? webApp.host : virtualHost);
        if (webContext == null) {
            return;
        }

        if (!deployedWebApps.add(webApp)) {
            return;
        }

        final Map<String, EJBRestServiceInfo> restEjbs = getRestEjbs(appInfo, webApp.moduleId);

        final ClassLoader classLoader = getClassLoader(webContext.getClassLoader());
        final Collection<Injection> injections = webContext.getInjections();
        final WebBeansContext owbCtx;
        if (webContext.getWebbeansContext() != null) {
            owbCtx = webContext.getWebbeansContext();
        } else {
            owbCtx = webContext.getAppContext().getWebBeansContext();
        }

        Context context = webContext.getJndiEnc();
        if (context == null) { // usually true since it is set in org.apache.tomee.catalina.TomcatWebAppBuilder.afterStart() and lookup(comp) fails
            context = webContext.getAppContext().getAppJndiContext();
        }

        final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);

        final Collection<Object> additionalProviders = new HashSet<>();
        addAppProvidersIfNeeded(appInfo, webApp, classLoader, additionalProviders);

        Collection<IdPropertiesInfo> pojoConfigurations = null; // done lazily
        try {
            boolean deploymentWithApplication = "true".equalsIgnoreCase(appInfo.properties.getProperty(OPENEJB_USE_APPLICATION_PROPERTY, APPLICATION_DEPLOYMENT));
            if (deploymentWithApplication) {
                Class<?> appClazz;
                for (final String app : webApp.restApplications) {
                    Application application;
                    boolean appSkipped = false;
                    String prefix = "/";

                    try {
                        appClazz = classLoader.loadClass(app);
                        application = Application.class.cast(appClazz.newInstance());
                        if (owbCtx != null && owbCtx.getBeanManagerImpl().isInUse()) {
                            try {
                                webContext.inject(application);
                            } catch (final Exception e) {
                                // not important since not required by the spec
                            }
                        }
                    } catch (final Exception e) {
                        throw new OpenEJBRestRuntimeException("can't create class " + app, e);
                    }

                    application = "true".equalsIgnoreCase(appInfo.properties.getProperty("openejb.cxf-rs.cache-application", "true"))
                            ? new InternalApplication(application) /* caches singletons and classes */ : application;

                    final Set<Class<?>> classes = new HashSet<>(application.getClasses());
                    final Set<Object> singletons = application.getSingletons();

                    if (classes.size() + singletons.size() == 0) {
                        appSkipped = true;
                    } else {
                        for (final Class<?> clazz : classes) {
                            if (isProvider(clazz)) {
                                additionalProviders.add(clazz);
                            } else if (!hasEjbAndIsNotAManagedBean(restEjbs, clazz.getName())) {
                                pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                                if (PojoUtil.findConfiguration(pojoConfigurations, clazz.getName()) != null) {
                                    deploymentWithApplication = false;
                                    logOldDeploymentUsage(clazz.getName());
                                }
                            }
                        }

                        if (deploymentWithApplication) { // don't do it if we detected we should use old deployment
                            for (final Object o : singletons) {
                                final Class<?> clazz = o.getClass();
                                if (isProvider(clazz)) {
                                    additionalProviders.add(o);
                                } else if (!hasEjbAndIsNotAManagedBean(restEjbs, clazz.getName())) {
                                    pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                                    if (PojoUtil.findConfiguration(pojoConfigurations, clazz.getName()) != null) {
                                        deploymentWithApplication = false;
                                        logOldDeploymentUsage(clazz.getName());
                                    }
                                }
                            }
                        }
                    }

                    if (deploymentWithApplication) { // don't do it if we detected we should use old deployment
                        final String path = appPrefix(webApp, appClazz);
                        if (path != null) {
                            prefix += path + wildcard;
                        } else {
                            prefix += wildcard;
                        }
                    }

                    if (deploymentWithApplication) { // don't do it if we detected we should use old deployment
                        if (appSkipped || application == null) {
                            application = !InternalApplication.class.isInstance(application) ? new InternalApplication(application) : application;

                            for (final String clazz : webApp.restClass) {
                                try {
                                    final Class<?> loaded = classLoader.loadClass(clazz);
                                    if (!isProvider(loaded)) {
                                        pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                                        if (PojoUtil.findConfiguration(pojoConfigurations, loaded.getName()) != null) {
                                            deploymentWithApplication = false;
                                            logOldDeploymentUsage(loaded.getName());
                                            break;
                                        }
                                        application.getClasses().add(loaded);
                                    } else {
                                        additionalProviders.add(loaded);
                                    }
                                } catch (final Exception e) {
                                    throw new OpenEJBRestRuntimeException("can't load class " + clazz, e);
                                }
                            }
                            if (deploymentWithApplication) {
                                addEjbToApplication(application, restEjbs);
                                if (!prefix.endsWith(wildcard)) {
                                    prefix += wildcard;
                                }
                            }
                        }

                        if (!application.getClasses().isEmpty() || !application.getSingletons().isEmpty()) {
                            pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                            deployApplication(appInfo, webApp.contextRoot, restEjbs, classLoader, injections, owbCtx, context, additionalProviders, pojoConfigurations, application, prefix);
                        }
                    }

                    if (!deploymentWithApplication) {
                        fullServletDeployment(appInfo, webApp, webContext, restEjbs, classLoader, injections, owbCtx, context, additionalProviders, pojoConfigurations);
                    }
                }

                if (webApp.restApplications.isEmpty()) {
                    final Application application = new InternalApplication(null);
                    for (final String clazz : webApp.restClass) {
                        try {
                            final Class<?> loaded = classLoader.loadClass(clazz);
                            if (!isProvider(loaded)) {
                                pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                                if (PojoUtil.findConfiguration(pojoConfigurations, loaded.getName()) != null) {
                                    deploymentWithApplication = false;
                                    logOldDeploymentUsage(loaded.getName());
                                    break;
                                }
                                application.getClasses().add(loaded);
                            } else {
                                additionalProviders.add(loaded);
                            }
                        } catch (final Exception e) {
                            throw new OpenEJBRestRuntimeException("can't load class " + clazz, e);
                        }
                    }
                    addEjbToApplication(application, restEjbs);

                    if (deploymentWithApplication) {
                        if (!application.getClasses().isEmpty() || !application.getSingletons().isEmpty()) {
                            final String path = appPrefix(webApp, application.getClass());
                            final String prefix;
                            if (path != null) {
                                prefix = "/" + path + wildcard;
                            } else {
                                prefix = "/" + wildcard;
                            }

                            pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                            deployApplication(appInfo, webApp.contextRoot, restEjbs, classLoader, injections, owbCtx, context, additionalProviders, pojoConfigurations, application, prefix);
                        }
                    } else {
                        fullServletDeployment(appInfo, webApp, webContext, restEjbs, classLoader, injections, owbCtx, context, additionalProviders, pojoConfigurations);
                    }
                }
            } else {
                fullServletDeployment(appInfo, webApp, webContext, restEjbs, classLoader, injections, owbCtx, context, additionalProviders, pojoConfigurations);
            }
        } finally {
            Thread.currentThread().setContextClassLoader(oldLoader);
        }
    }

    private void addAppProvidersIfNeeded(AppInfo appInfo, WebAppInfo webApp, ClassLoader classLoader, Collection<Object> additionalProviders) {
        if (useDiscoveredProviders(appInfo)) {
            final Set<String> jaxRsProviders = new HashSet<>(webApp.jaxRsProviders);
            jaxRsProviders.addAll(appInfo.jaxRsProviders);
            additionalProviders.addAll(appProviders(jaxRsProviders, classLoader));
        }
    }

    private void addEjbToApplication(final Application application, final Map<String, EJBRestServiceInfo> restEjbs) {
        for (final Map.Entry<String, EJBRestServiceInfo> ejb : restEjbs.entrySet()) {
            application.getClasses().add(ejb.getValue().context.getBeanClass());
        }
    }

    private void fullServletDeployment(final AppInfo appInfo, final WebAppInfo webApp, final WebContext webContext,
                                       final Map<String, EJBRestServiceInfo> restEjbs, final ClassLoader classLoader,
                                       final Collection<Injection> injections, final WebBeansContext owbCtx,
                                       final Context context, final Collection<Object> additionalProviders,
                                       final Collection<IdPropertiesInfo> initPojoConfigurations) {
        // The spec says:
        //
        // "The resources and providers that make up a JAX-RS application are configured via an application-supplied
        // subclass of Application. An implementation MAY provide alternate mechanisms for locating resource
        // classes and providers (e.g. runtime class scanning) but use of Application is the only portable means of
        //  configuration."
        //
        //  The choice here is to deploy using the Application if it exists or to use the scanned classes
        //  if there is no Application.
        //
        //  Like this providing an Application subclass user can totally control deployed services.

        Collection<IdPropertiesInfo> pojoConfigurations = null;
        boolean useApp = false;
        String appPrefix = webApp.contextRoot;
        for (final String app : webApp.restApplications) {
            appPrefix = webApp.contextRoot; // if multiple application classes reinit it
            if (!appPrefix.endsWith("/")) {
                appPrefix += "/";
            }

            final Application appInstance;
            final Class<?> appClazz;
            try {
                appClazz = classLoader.loadClass(app);
                appInstance = Application.class.cast(appClazz.newInstance());
                if (owbCtx.getBeanManagerImpl().isInUse()) {
                    try {
                        webContext.inject(appInstance);
                    } catch (final Exception e) {
                        // not important since not required by the spec
                    }
                }
            } catch (final Exception e) {
                throw new OpenEJBRestRuntimeException("can't create class " + app, e);
            }

            final String path = appPrefix(webApp, appClazz);
            if (path != null) {
                appPrefix += path;
            }

            final Set<Class<?>> classes = appInstance.getClasses();
            final Set<Object> singletons = appInstance.getSingletons();

            // look for providers
            for (final Class<?> clazz : classes) {
                if (isProvider(clazz)) {
                    additionalProviders.add(clazz);
                }
            }
            for (final Object obj : singletons) {
                if (obj != null && isProvider(obj.getClass())) {
                    additionalProviders.add(obj);
                }
            }

            for (final Object o : singletons) {
                if (o == null || additionalProviders.contains(o)) {
                    continue;
                }

                if (hasEjbAndIsNotAManagedBean(restEjbs, o.getClass().getName())) {
                    // no more a singleton if the ejb is not a singleton...but it is a weird case
                    deployEJB(appInfo.appId, webApp.contextRoot, appPrefix, restEjbs.get(o.getClass().getName()).context, additionalProviders, appInfo.services);
                } else {
                    pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                    deploySingleton(appInfo.appId, webApp.contextRoot, appPrefix, o, appInstance, classLoader, additionalProviders,
                            new ServiceConfiguration(PojoUtil.findConfiguration(pojoConfigurations, o.getClass().getName()), appInfo.services));
                }
            }

            for (final Class<?> clazz : classes) {
                if (additionalProviders.contains(clazz)) {
                    continue;
                }

                if (hasEjbAndIsNotAManagedBean(restEjbs, clazz.getName())) {
                    deployEJB(appInfo.appId, webApp.contextRoot, appPrefix, restEjbs.get(clazz.getName()).context, additionalProviders, appInfo.services);
                } else {
                    pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                    deployPojo(appInfo.appId, webApp.contextRoot, appPrefix, clazz, appInstance, classLoader, injections, context, owbCtx, additionalProviders,
                            new ServiceConfiguration(PojoUtil.findConfiguration(pojoConfigurations, clazz.getName()), appInfo.services));
                }
            }

            useApp = useApp || classes.size() + singletons.size() > 0;
            LOGGER.info("REST application deployed: " + app);
        }

        if (!useApp) {
            if (webApp.restApplications.isEmpty() || webApp.restApplications.size() > 1) {
                appPrefix = webApp.contextRoot;
            } // else keep application prefix

            final Set<String> restClasses = new HashSet<>(webApp.restClass);
            restClasses.addAll(webApp.ejbRestServices);

            for (final String clazz : restClasses) {
                if (restEjbs.containsKey(clazz)) {
                    final BeanContext ctx = restEjbs.get(clazz).context;
                    if (hasEjbAndIsNotAManagedBean(restEjbs, clazz)) {
                        deployEJB(appInfo.appId, webApp.contextRoot, appPrefix, restEjbs.get(clazz).context, additionalProviders, appInfo.services);
                    } else {
                        deployPojo(appInfo.appId, webApp.contextRoot, appPrefix, ctx.getBeanClass(), null, ctx.getClassLoader(), ctx.getInjections(), context,
                                owbCtx, additionalProviders, new ServiceConfiguration(ctx.getProperties(), appInfo.services));
                    }
                } else {
                    try {
                        final Class<?> loadedClazz = classLoader.loadClass(clazz);
                        pojoConfigurations = PojoUtil.findPojoConfig(pojoConfigurations, appInfo, webApp);
                        deployPojo(appInfo.appId, webApp.contextRoot, appPrefix, loadedClazz, null, classLoader, injections, context, owbCtx,
                                additionalProviders,
                                new ServiceConfiguration(PojoUtil.findConfiguration(pojoConfigurations, loadedClazz.getName()), appInfo.services));
                    } catch (final ClassNotFoundException e) {
                        throw new OpenEJBRestRuntimeException("can't find class " + clazz, e);
                    }
                }
            }
        }
    }

    protected static void logOldDeploymentUsage(final String clazz) {
        LOGGER.info("Using deployment by endpoint instead of by application for JAXRS deployment because an old configuration (by class/ejb) was found on " + clazz);
    }

    private void deployApplication(final AppInfo appInfo, final String contextRoot, final Map<String, EJBRestServiceInfo> restEjbs, final ClassLoader classLoader, final Collection<Injection> injections, final WebBeansContext owbCtx, final Context context, final Collection<Object> additionalProviders, final Collection<IdPropertiesInfo> pojoConfigurations, final Application application, final String prefix) {
        // get configuration
        Properties configuration;
        if (InternalApplication.class.equals(application.getClass())) {
            final Application original = InternalApplication.class.cast(application).getOriginal();
            if (original == null) {
                configuration = PojoUtil.findConfiguration(pojoConfigurations, "jaxrs-application");
            } else {
                configuration = PojoUtil.findConfiguration(pojoConfigurations, original.getClass().getName());
            }
        } else {
            configuration = PojoUtil.findConfiguration(pojoConfigurations, application.getClass().getName());
        }
        if (configuration == null) { // try a constant (common in half of cases)
            configuration = PojoUtil.findConfiguration(pojoConfigurations, "jaxrs-application");
        }

        if (configuration != null) {
            LOGGER.info("Registered JAX-RS Configuration: " + configuration);
        }

        final String base = getAddress(contextRoot);
        final String nopath;
        if (base.endsWith("/") && prefix.startsWith("/")) {
            nopath = base + prefix.substring(1);
        } else {
            nopath = base + prefix;
        }

        final RsHttpListener listener = createHttpListener();
        final String host = findHost(contextRoot, appInfo.webApps);
        final RsRegistry.AddressInfo address = rsRegistry.createRsHttpListener(appInfo.appId, contextRoot, listener, classLoader, nopath.substring(NOPATH_PREFIX.length() - 1), host, auth, realm);

        services.add(new DeployedService(address.complete, contextRoot, application.getClass().getName(), appInfo.appId));
        listener.deployApplication(application, address.complete.substring(0, address.complete.length() - wildcard.length()), nopath.substring(NOPATH_PREFIX.length(), nopath.length() - wildcard.length()), additionalProviders, restEjbs, // app config
                classLoader, injections, context, owbCtx, // injection/webapp context
                new ServiceConfiguration(configuration, appInfo.services)); // deployment config
    }

    private String findHost(final String context, final Collection<WebAppInfo> webs) {
        for (final WebAppInfo web : webs) {
            if (context.equals(web.contextRoot)) {
                return web.host != null ? web.host : virtualHost;
            }
        }
        return virtualHost;
    }

    private static String appPrefix(final WebAppInfo info, final Class<?> appClazz) {
        StringBuilder builder = null;

        // no annotation, try servlets
        for (final ServletInfo s : info.servlets) {
            if (s.mappings.isEmpty()) {
                continue;
            }

            String mapping = null;

            final String name = appClazz.getName();
            if (name.equals(s.servletClass) || name.equals(s.servletName) || "javax.ws.rs.core.Application ".equals(s.servletName)) {
                mapping = s.mappings.iterator().next();
            } else {
                for (final ParamValueInfo pvi : s.initParams) {
                    if ("javax.ws.rs.Application".equals(pvi.name) || Application.class.getName().equals(pvi.name)) {
                        mapping = s.mappings.iterator().next();
                        break;
                    }
                }
            }

            if (mapping != null) {
                if (mapping.endsWith("/*")) {
                    mapping = mapping.substring(0, mapping.length() - 2);
                }
                if (mapping.startsWith("/")) {
                    mapping = mapping.substring(1);
                }

                builder = new StringBuilder();
                builder.append(mapping);
                break;
            }
        }
        if (builder != null) { // https://issues.apache.org/jira/browse/CXF-5702
            return builder.toString();
        }

        // annotation
        final ApplicationPath path = appClazz.getAnnotation(ApplicationPath.class);
        if (path != null) {
            String appPath = path.value();
            if (appPath.endsWith("*")) {
                appPath = appPath.substring(0, appPath.length() - 1);
            }

            builder = new StringBuilder();
            if (appPath.startsWith("/")) {
                builder.append(appPath.substring(1));
            } else {
                builder.append(appPath);
            }
        }

        if (builder == null) {
            return null;
        }
        return builder.toString();
    }

    private static <T> boolean isProvider(final Class<T> clazz) {
        return MessageBodyReader.class.isAssignableFrom(clazz) ||
                MessageBodyWriter.class.isAssignableFrom(clazz) ||
                ParamConverter.class.isAssignableFrom(clazz) ||
                ContainerRequestFilter.class.isAssignableFrom(clazz) ||
                ContainerResponseFilter.class.isAssignableFrom(clazz) ||
                ReaderInterceptor.class.isAssignableFrom(clazz) ||
                WriterInterceptor.class.isAssignableFrom(clazz) ||
                ParamConverterProvider.class.isAssignableFrom(clazz) ||
                ContextResolver.class.isAssignableFrom(clazz) ||
                new MetaAnnotatedClass<>(clazz).isAnnotationPresent(Provider.class);
    }

    private boolean hasEjbAndIsNotAManagedBean(final Map<String, EJBRestServiceInfo> restEjbs, final String clazz) {
        return restEjbs.containsKey(clazz) && !BeanType.MANAGED.equals(restEjbs.get(clazz).context.getComponentType());
    }

    private boolean useDiscoveredProviders(final AppInfo appInfo) {
        final String value = appInfo.properties.getProperty(OPENEJB_JAXRS_PROVIDERS_AUTO_PROP);
        if (value != null) {
            return "true".equalsIgnoreCase(value.trim());
        }
        return SystemInstance.get().getOptions().get(OPENEJB_JAXRS_PROVIDERS_AUTO_PROP, true);
    }

    private Collection<Object> appProviders(final Collection<String> jaxRsProviders, final ClassLoader classLoader) {
        final Collection<Object> additionalProviders = new HashSet<>();
        for (final String name : jaxRsProviders) {
            try {
                final Class<?> providerClass = classLoader.loadClass(name);
                if (providerClass.getAnnotation(Deprecated.class) != null) {
                    continue;
                }
                additionalProviders.add(providerClass);
            } catch (final ClassNotFoundException e) {
                LOGGER.warning("can't load '" + name + "'", e);
            }
        }
        return additionalProviders;
    }

    public void afterApplicationCreated(@Observes final AssemblerAfterApplicationCreated event) {
        if (!enabled)
            return;

        final AppInfo appInfo = event.getApp();
        if ("false".equalsIgnoreCase(appInfo.properties.getProperty("openejb.jaxrs.on", "true"))) {
            return;
        }

        quickCheckIfOldDeploymentShouldBeUsedFromEjbConfig(appInfo);

        if (deployedApplications.add(appInfo)) {
            if (appInfo.webApps.size() == 0) {
                final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
                final ClassLoader appClassLoader = getClassLoader(containerSystem.getAppContext(appInfo.appId).getClassLoader());
                Thread.currentThread().setContextClassLoader(appClassLoader);

                try {
                    final Map<String, EJBRestServiceInfo> restEjbs = getRestEjbs(appInfo, null);
                    if (restEjbs.isEmpty()) {
                        return;
                    }

                    final Collection<Object> providers;
                    if (useDiscoveredProviders(appInfo)) {
                        providers = appProviders(appInfo.jaxRsProviders, appClassLoader);
                    } else {
                        providers = new ArrayList<>();
                    }

                    if ("true".equalsIgnoreCase(appInfo.properties.getProperty(OPENEJB_USE_APPLICATION_PROPERTY, APPLICATION_DEPLOYMENT))) {
                        final Application application = new InternalApplication(null);
                        addEjbToApplication(application, restEjbs);

                        // merge configurations at app level since a single deployment is available
                        final List<IdPropertiesInfo> pojoConfigurations = new ArrayList<>();
                        BeanContext comp = null;
                        for (final EjbJarInfo ejbJar : appInfo.ejbJars) {
                            for (final EnterpriseBeanInfo bean : ejbJar.enterpriseBeans) {
                                if (comp != null) {
                                    break;
                                }
                                if (bean.ejbClass.equals(BeanContext.Comp.class.getName())) {
                                    comp = containerSystem.getBeanContext(bean.ejbDeploymentId);
                                    break;
                                }
                            }
                            if (ejbJar.pojoConfigurations != null) {
                                pojoConfigurations.addAll(ejbJar.pojoConfigurations);
                            }
                        }
                        if (appInfo.pojoConfigurations != null) {
                            pojoConfigurations.addAll(appInfo.pojoConfigurations);
                        }

                        final Map.Entry<String, EJBRestServiceInfo> next = restEjbs.entrySet().iterator().next();
                        if (comp == null) {
                            comp = next.getValue().context;
                        }

                        deployApplication(appInfo, next.getValue().path, restEjbs, comp.getClassLoader(), comp.getInjections(),
                                containerSystem.getAppContext(appInfo.appId).getWebBeansContext(), comp.getJndiContext(),
                                providers, pojoConfigurations, application, wildcard);
                    } else {
                        for (final Map.Entry<String, EJBRestServiceInfo> ejb : restEjbs.entrySet()) {
                            final BeanContext ctx = ejb.getValue().context;
                            if (BeanType.MANAGED.equals(ctx.getComponentType())) {
                                deployPojo(appInfo.appId, "", ejb.getValue().path, ctx.getBeanClass(), null, ctx.getClassLoader(), ctx.getInjections(),
                                        ctx.getJndiContext(),
                                        containerSystem.getAppContext(appInfo.appId).getWebBeansContext(),
                                        providers, new ServiceConfiguration(ctx.getProperties(), appInfo.services));
                            } else {
                                deployEJB(appInfo.appId, "", ejb.getValue().path, ctx, providers, appInfo.services);
                            }
                        }
                    }
                } finally {
                    Thread.currentThread().setContextClassLoader(oldLoader);
                }
            } else {
                for (final WebAppInfo webApp : appInfo.webApps) {
                    afterApplicationCreated(appInfo, webApp);
                }
            }
        }
    }

    private void quickCheckIfOldDeploymentShouldBeUsedFromEjbConfig(final AppInfo appInfo) {
        // if forced don't update anything
        if (appInfo.properties.getProperty(OPENEJB_USE_APPLICATION_PROPERTY) != null) {
            return;
        }

        for (final EjbJarInfo ejbJar : appInfo.ejbJars) {
            for (final EnterpriseBeanInfo bean : ejbJar.enterpriseBeans) {
                if (bean.restService) {
                    final BeanContext beanContext = containerSystem.getBeanContext(bean.ejbDeploymentId);
                    if (beanContext == null) { // ear
                        continue;
                    }

                    if (containsJaxRsConfiguration(beanContext.getProperties())) {
                        appInfo.properties.setProperty(OPENEJB_USE_APPLICATION_PROPERTY, "false");
                        logOldDeploymentUsage(bean.ejbClass);
                        return; // no need to look further
                    }
                }
            }
        }
    }

    protected abstract boolean containsJaxRsConfiguration(final Properties properties);

    protected Map<String, EJBRestServiceInfo> getRestEjbs(final AppInfo appInfo, final String webapp) {
        final Map<String, BeanContext> beanContexts = new HashMap<>();
        for (final EjbJarInfo ejbJar : appInfo.ejbJars) {
            if (ejbJar.webapp && webapp != null && !ejbJar.moduleId.equals(webapp)) {
                continue;
            }

            for (final EnterpriseBeanInfo bean : ejbJar.enterpriseBeans) {
                if (bean.restService) {
                    final BeanContext beanContext = containerSystem.getBeanContext(bean.ejbDeploymentId);
                    if (beanContext == null) {
                        continue;
                    }

                    beanContexts.put(bean.ejbClass, beanContext);
                }
            }
        }

        final Map<String, EJBRestServiceInfo> restEjbs = new HashMap<>();
        for (final WebAppInfo webApp : appInfo.webApps) {
            for (final String ejb : webApp.ejbRestServices) {
                if (beanContexts.containsKey(ejb)) {
                    restEjbs.put(ejb, new EJBRestServiceInfo(webApp.contextRoot, beanContexts.get(ejb)));
                } // else ear probably
            }
        }
        for (final Map.Entry<String, BeanContext> ejbs : beanContexts.entrySet()) {
            final String clazz = ejbs.getKey();
            if (!restEjbs.containsKey(clazz)) {
                // null is important, it means there is no webroot path in standalone
                String context = null;
                if (!OLD_WEBSERVICE_DEPLOYMENT) {
                    if (appInfo.appId != null && !appInfo.appId.isEmpty()) {
                        context = appInfo.appId;
                    } else {
                        context = ejbs.getValue().getModuleName();
                    }
                }
                restEjbs.put(clazz, new EJBRestServiceInfo(context, beanContexts.get(clazz)));
            }
        }
        beanContexts.clear();

        return restEjbs;
    }

    private void deploySingleton(final String appId, final String web, final String contextRoot, final Object o, final Application appInstance, final ClassLoader classLoader,
                                 final Collection<Object> additionalProviders, final ServiceConfiguration configuration) {
        final String nopath = getAddress(contextRoot, o.getClass());
        final RsHttpListener listener = createHttpListener();
        final RsRegistry.AddressInfo address = rsRegistry.createRsHttpListener(appId, web, listener, classLoader, nopath.substring(NOPATH_PREFIX.length() - 1), virtualHost, auth, realm);

        services.add(new DeployedService(address.complete, web, o.getClass().getName(), appId));
        listener.deploySingleton(contextRoot, getFullContext(address.base, contextRoot), o, appInstance, additionalProviders, configuration);

        LOGGER.info("deployed REST singleton: " + o);
    }

    private void deployPojo(final String appId, final String web, final String contextRoot, final Class<?> loadedClazz, final Application app,
                            final ClassLoader classLoader, final Collection<Injection> injections,
                            final Context context, final WebBeansContext owbCtx, final Collection<Object> additionalProviders, final ServiceConfiguration config) {
        if (loadedClazz.isInterface()) {
            return;
        }

        final String nopath = getAddress(contextRoot, loadedClazz);
        final RsHttpListener listener = createHttpListener();
        final RsRegistry.AddressInfo address = rsRegistry.createRsHttpListener(appId, web, listener, classLoader, nopath.substring(NOPATH_PREFIX.length() - 1), virtualHost, auth, realm);

        services.add(new DeployedService(address.complete, contextRoot, loadedClazz.getName(), appId));
        listener.deployPojo(classLoader, contextRoot, getFullContext(address.base, contextRoot), loadedClazz, app, injections, context, owbCtx,
                additionalProviders, config);

        LOGGER.info("REST Service: " + address.complete + "  -> Pojo " + loadedClazz.getName());
    }

    private void deployEJB(final String appId, final String web, final String context, final BeanContext beanContext, final Collection<Object> additionalProviders,
                           final Collection<ServiceInfo> serviceInfos) {
        final String nopath = getAddress(context, beanContext.getBeanClass());
        final RsHttpListener listener = createHttpListener();
        final RsRegistry.AddressInfo address = rsRegistry.createRsHttpListener(appId, web, listener, beanContext.getClassLoader(), nopath.substring(NOPATH_PREFIX.length() - 1), virtualHost, auth, realm);

        services.add(new DeployedService(address.complete, context, beanContext.getBeanClass().getName(), appId));
        listener.deployEJB(context, getFullContext(address.base, context), beanContext,
                additionalProviders, new ServiceConfiguration(beanContext.getProperties(), serviceInfos));

        LOGGER.info("REST Service: " + address.complete + "  -> EJB " + beanContext.getEjbName());
    }

    /**
     * It creates the service container (http listener).
     *
     * @return the service container
     */
    protected abstract RsHttpListener createHttpListener();

    private static String getFullContext(final String address, final String context) {
        if (context == null) {
            return address;
        }
        if (context.isEmpty() && address.contains("/")) {
            return address.substring(0, address.lastIndexOf("/"));
        }

        // context can get the app path too
        // so keep only web context without /
        String webCtx = context;
        while (webCtx.startsWith("/")) {
            webCtx = webCtx.substring(1);
        }

        // get root path ending with /
        try {
            final URL url = new URL(address);
            final int port = url.getPort();
            if (port > 0) {
                return url.getProtocol() + "://" + url.getHost() + ":" + port + "/" + webCtx;
            } else {
                return url.getProtocol() + "://" + url.getHost() + "/" + webCtx;
            }
        } catch (final MalformedURLException e) {
            throw new OpenEJBRestRuntimeException("bad url: " + address, e);
        }
    }

    private Class<?> findPath(final Class<?> clazz) {
        Class<?> usedClass = clazz;
        while (usedClass.getAnnotation(Path.class) == null && usedClass.getSuperclass() != null) {
            usedClass = usedClass.getSuperclass();
        }
        return usedClass;
    }

    private String getAddress(final String context, final Class<?> clazz) {
        String root = getAddress(context);

        Class<?> usedClass = findPath(clazz);
        if (usedClass == null || Object.class.equals(usedClass)) { // try interfaces
            final Class<?>[] itfs = clazz.getInterfaces();
            if (itfs != null) {
                for (final Class<?> c : itfs) {
                    usedClass = findPath(c);
                    if (usedClass.getAnnotation(Path.class) != null) {
                        break;
                    }
                }
            }

        }
        if (usedClass == null || usedClass.getAnnotation(Path.class) == null) {
            throw new IllegalArgumentException("no @Path annotation on " + clazz.getName());
        }

        String builtUrl = null;
        try {
            builtUrl = UriBuilder.fromUri(new URI(root)).path(usedClass).build().toURL().toString();
            return replaceParams(builtUrl); // pathparam at class level
        } catch (final IllegalArgumentException iae) {
            if (builtUrl != null) {
                return builtUrl;
            }

            // try to do it manually with @Path on the class
            Class<?> current = usedClass;
            while (current != null && !Object.class.equals(current)) {
                final Path path = current.getAnnotation(Path.class);
                if (path != null) {
                    String classPath = path.value();
                    if (classPath.startsWith("/")) {
                        classPath = classPath.substring(1);
                    }
                    if (!root.endsWith("/")) {
                        root = root + "/";
                    }
                    return replaceParams(root + classPath);
                }
                current = current.getSuperclass();
            }

            throw new OpenEJBRestRuntimeException("can't built the service mapping for service '" + usedClass.getName() + "'", iae);
        } catch (final MalformedURLException e) {
            throw new OpenEJBRestRuntimeException("url is malformed", e);
        } catch (final URISyntaxException e) {
            throw new OpenEJBRestRuntimeException("uri syntax is not correct", e);
        }
    }

    private String getAddress(final String context) {
        String root = NOPATH_PREFIX;
        if (context != null) {
            if (context.startsWith("/")) {
                root += context.substring(1);
            } else {
                root += context;
            }
        }
        return root;
    }

    // this mean not really conflicting mappings (rest/servlet and so on) can be conflicting
    // a good solution is to handle a unique rest servlet managing the routing
    private String replaceParams(final String url) {
        final String managedUrl = url.replaceAll("\\{[^}]*\\}.*", wildcard);
        if (managedUrl.endsWith(wildcard)) {
            return managedUrl;
        }
        return managedUrl + "/" + wildcard;
    }

    private void undeployRestObject(final String appId, final String context) {
        HttpListener listener = rsRegistry.removeListener(appId, context);
        if (listener != null) {

            if (BasicAuthHttpListenerWrapper.class.isInstance(listener)) {
                listener = BasicAuthHttpListenerWrapper.class.cast(listener).getHttpListener();
            }

            checkUndeploy(listener);
        }
    }

    private void checkUndeploy(final HttpListener listener) {
        if (RsHttpListener.class.isInstance(listener)) {
            RsHttpListener.class.cast(listener).undeploy();
        }
    }

    private static ClassLoader getClassLoader(final ClassLoader classLoader) {
        ClassLoader cl = classLoader;
        if (cl == null) {
            cl = Thread.currentThread().getContextClassLoader();
        }
        if (cl == null) {
            cl = RESTService.class.getClassLoader();
        }
        return cl;
    }

    public void undeploy(@Observes final AssemblerBeforeApplicationDestroyed event) {
        final AppInfo app = event.getApp();
        final boolean removed = deployedApplications.remove(app);
        if (removed) {
            for (final WebAppInfo webApp : app.webApps) {
                final List<DeployedService> toRemove = new ArrayList<>();
                for (final DeployedService service : services) {
                    if (service.isInWebApp(app.appId, webApp)) {
                        undeployRestObject(app.appId, service.address);
                        toRemove.add(service);
                    }
                }
                services.removeAll(toRemove);
                deployedWebApps.remove(webApp);
            }
        }
    }

    @Override
    public void start() throws ServiceException {
        SystemInstance.get().setComponent(RESTService.class, this);

        beforeStart();

        containerSystem = (CoreContainerSystem) SystemInstance.get().getComponent(ContainerSystem.class);
        assembler = SystemInstance.get().getComponent(Assembler.class);
        if (assembler != null) {
            SystemInstance.get().addObserver(this);
            for (final AppInfo appInfo : assembler.getDeployedApplications()) {
                final AppContext appContext = containerSystem.getAppContext(appInfo.appId);
                afterApplicationCreated(new AssemblerAfterApplicationCreated(appInfo, appContext, null));
            }
        }
    }

    protected void beforeStart() {
        rsRegistry = SystemInstance.get().getComponent(RsRegistry.class);
        if (rsRegistry == null && SystemInstance.get().getComponent(HttpListenerRegistry.class) != null) {
            rsRegistry = new RsRegistryImpl();
        }
    }

    @Override
    public void stop() throws ServiceException {
        if (assembler != null) {
            SystemInstance.get().removeObserver(this);
            for (final AppInfo appInfo : new ArrayList<>(deployedApplications)) {
                undeploy(new AssemblerBeforeApplicationDestroyed(appInfo, null));
            }
        }

        for (final DeployedService service : services) {
            undeployRestObject(service.appId, service.address);
        }
    }

    @Override
    public void service(final InputStream in, final OutputStream out) throws ServiceException, IOException {
        throw new UnsupportedOperationException(getClass().getName() + " cannot be invoked directly");
    }

    @Override
    public void service(final Socket socket) throws ServiceException, IOException {
        throw new UnsupportedOperationException(getClass().getName() + " cannot be invoked directly");
    }

    @Override
    public String getIP() {
        return IP;
    }

    @Override
    public int getPort() {
        return PORT;
    }

    @Override
    public void init(final Properties props) throws Exception {
        if (props == null) {
            return;
        }

        virtualHost = props.getProperty("virtualHost", "localhost");
        auth = props.getProperty("auth", "NONE");
        realm = props.getProperty("realm", "PropertiesLogin");
        enabled = ServiceManager.isEnabled(props);
    }

    public String getVirtualHost() {
        return virtualHost;
    }

    public void setVirtualHost(final String virtualHost) {
        this.virtualHost = virtualHost;
    }

    public String getAuth() {
        return auth;
    }

    public void setAuth(final String auth) {
        this.auth = auth;
    }

    public String getRealm() {
        return realm;
    }

    public void setRealm(final String realm) {
        this.realm = realm;
    }

    // look WebServiceHelperImpl before updating it
    public static class DeployedService {

        public String address;
        public String webapp;
        public String origin;
        public String appId;

        public DeployedService(final String address, final String webapp, final String origin, final String appId) {
            this.address = address;
            this.webapp = webapp;
            this.origin = origin;
            this.appId = appId;
        }

        public boolean isInWebApp(final String appId, final WebAppInfo webApp) { // we support paralell deployments so we need app (versionned) + webapp check
            final boolean appTest = !(appId == null && this.appId != null) && !(appId != null && !appId.equals(this.appId));
            return appTest && cleanWeb(webapp).equals(cleanWeb(webApp.contextRoot));
        }

        private String cleanWeb(final String s) {
            if (s == null) {
                return "";
            }
            if (s.startsWith("/")) {
                return cleanWeb(s.substring(1));
            }

            final int i = s.indexOf("##");
            if (i > 0) {
                return cleanWeb(s.substring(0, i));
            }

            if ("ROOT".equals(s)) {
                return "";
            }
            return s;
        }
    }

    public List<DeployedService> getServices() {
        return services;
    }

    public String getWildcard() {
        return wildcard;
    }
}
