/*
 * Copyright 2011 Red Hat, Inc. and/or its affiliates.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA
 */
package com.jboss.datagrid.endpoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.ApplicationParameter;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityCollection;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.tomcat.InstanceManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.jboss.as.controller.services.path.PathManager;
import org.jboss.as.security.plugins.SecurityDomainContext;
import org.jboss.as.web.VirtualHost;
import org.jboss.as.web.deployment.WebCtxLoader;
import org.jboss.as.web.security.JBossWebRealm;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.msc.service.Service;
import org.jboss.msc.service.StartContext;
import org.jboss.msc.service.StartException;
import org.jboss.msc.service.StopContext;
import org.jboss.msc.value.InjectedValue;
import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
import org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap;
import org.jboss.security.negotiation.NegotiationAuthenticator;

import static com.jboss.datagrid.EndpointLogger.ROOT_LOGGER;

/**
 * A service which starts the REST web application
 *
 * @author Tristan Tarrant <ttarrant@redhat.com>
 * @since 6.0
 */
public class RestService implements Service<Context> {
   private static final String DEFAULT_VIRTUAL_SERVER = "default-host";
   private static final String HOME_DIR = "jboss.home.dir";
   private static final Logger log = Logger.getLogger("com.jboss.datagrid");
   private static final String DEFAULT_CONTEXT_PATH = "";
   private final StandardContext context;
   private final InjectedValue<PathManager> pathManagerInjector = new InjectedValue<PathManager>();
   private final InjectedValue<VirtualHost> hostInjector = new InjectedValue<VirtualHost>();
   private final InjectedValue<EmbeddedCacheManager> cacheManagerInjector = new InjectedValue<EmbeddedCacheManager>();
   private final InjectedValue<SecurityDomainContext> securityDomainContextInjector = new InjectedValue<SecurityDomainContext>();
   private Method cacheManagerSetter;
   private final ModelNode config;
   private final String virtualServer;
   private final String path;
   private final String serverName;
   private final String securityDomain;
   private final String authMethod;
   private final SecurityMode securityMode;
   private PathManager.Callback.Handle callbackHandle;

   public RestService(ModelNode config) {
      this.config = config.clone();

      context = new StandardContext();
      virtualServer = this.config.hasDefined(ModelKeys.VIRTUAL_SERVER) ? this.config.get(ModelKeys.VIRTUAL_SERVER)
            .asString() : DEFAULT_VIRTUAL_SERVER;
      path = this.config.hasDefined(ModelKeys.CONTEXT_PATH) ? cleanContextPath(this.config.get(ModelKeys.CONTEXT_PATH)
            .asString()) : DEFAULT_CONTEXT_PATH;
      serverName = config.hasDefined(ModelKeys.NAME) ? config.get(ModelKeys.NAME).asString() : "";
      securityDomain = config.hasDefined(ModelKeys.SECURITY_DOMAIN) ? config.get(ModelKeys.SECURITY_DOMAIN).asString()
            : null;
      authMethod = config.hasDefined(ModelKeys.AUTH_METHOD) ? config.get(ModelKeys.AUTH_METHOD).asString() : "BASIC";
      securityMode = config.hasDefined(ModelKeys.SECURITY_MODE) ? SecurityMode.valueOf(config.get(
            ModelKeys.SECURITY_MODE).asString()) : SecurityMode.READ_WRITE;

      // Obtain the setter for injecting the EmbbededCacheManager into the Rest server
      try {
         Class<?> cls = Class.forName("org.infinispan.rest.ManagerInstance", true, getClass().getClassLoader());
         cacheManagerSetter = cls.getMethod("instance_$eq", EmbeddedCacheManager.class);
      } catch (Exception e) {
         throw ROOT_LOGGER.cannotLocateManagerInstance(e);
      }
   }

   private static String cleanContextPath(String s) {
      if (s.endsWith("/")) {
         return s.substring(0, s.length() - 1);
      } else {
         return s;
      }
   }

   /** {@inheritDoc} */
   @Override
   public synchronized void start(StartContext startContext) throws StartException {
      ROOT_LOGGER.endpointStarting("REST");
      EmbeddedCacheManager cacheManager = cacheManagerInjector.getValue();
      try {
         cacheManagerSetter.invoke(null, cacheManager);
      } catch (Exception e) {
         throw ROOT_LOGGER.restCacheManagerInjectionFailed(e);
      }
      try {
         context.setPath(path);
         context.addLifecycleListener(new RestContextConfig());

         context.setDocBase(pathManagerInjector.getValue().resolveRelativePathEntry("rest", HOME_DIR));
         callbackHandle = pathManagerInjector.getValue().registerCallback(HOME_DIR,
               PathManager.ReloadServerCallback.create(), PathManager.Event.UPDATED, PathManager.Event.REMOVED);

         final Loader loader = new WebCtxLoader(this.getClass().getClassLoader());
         Host host = hostInjector.getValue().getHost();
         loader.setContainer(host);
         context.setLoader(loader);
         context.setInstanceManager(new LocalInstanceManager());

         // Configuration for Resteasy bootstrap
         addContextApplicationParameter(context, "resteasy.resources", "org.infinispan.rest.Server");
         addContextApplicationParameter(context, "resteasy.use.builtin.providers", "true");

         // Setup the Resteasy bootstrap listener
         context.addApplicationListener(ResteasyBootstrap.class.getName());

         // Set the welcome file
         context.setReplaceWelcomeFiles(true);
         context.addWelcomeFile("index.html");

         // Add the default servlet for managing static content
         Wrapper wrapper = context.createWrapper();
         wrapper.setName("default");
         wrapper.setServletClass("org.apache.catalina.servlets.DefaultServlet");
         context.addChild(wrapper);
         context.addServletMapping("/", "default");
         context.addMimeMapping("html", "text/html");
         context.addMimeMapping("jpg", "image/jpeg");

         // Add the Resteasy servlet dispatcher for handling REST requests
         HttpServletDispatcher hsd = new HttpServletDispatcher();
         Wrapper hsdWrapper = context.createWrapper();
         hsdWrapper.setName("Resteasy");
         hsdWrapper.setServlet(hsd);
         hsdWrapper.setServletClass(hsd.getClass().getName());
         context.addChild(hsdWrapper);

         context.addServletMapping("/rest/*", "Resteasy");

         if (securityDomain != null) {
            configureContextSecurity();
         }

         host.addChild(context);
         context.create();
      } catch (Exception e) {
         throw ROOT_LOGGER.restContextCreationFailed(e);
      }
      try {
         context.start();
         ROOT_LOGGER.httpEndpointStarted("REST", path, "rest");
      } catch (LifecycleException e) {
         throw ROOT_LOGGER.restContextStartFailed(e);
      }
   }

   private void configureContextSecurity() {
      SecurityConstraint constraint = new SecurityConstraint();
      SecurityCollection webCollection = new SecurityCollection();
      webCollection.addPattern("/rest/*");
      switch (securityMode) {
      case WRITE:
         // protect all writes
         webCollection.addMethod("PUT");
         webCollection.addMethod("POST");
         webCollection.addMethod("DELETE");
         break;
      case READ_WRITE:
         // protect all methods
         break;
      }
      constraint.addCollection(webCollection);
      constraint.setAuthConstraint(true);
      constraint.addAuthRole("REST");
      context.addConstraint(constraint);
      LoginConfig login = new LoginConfig();
      login.setAuthMethod(authMethod);
      login.setRealmName("ApplicationRealm");
      context.setLoginConfig(login);
      context.addSecurityRole("REST");

      JBossWebRealm realm = new JBossWebRealm();
      SecurityDomainContext securityDomainContext = securityDomainContextInjector.getValue();
      realm.setAuthenticationManager(securityDomainContext.getAuthenticationManager());
      realm.setAuthorizationManager(securityDomainContext.getAuthorizationManager());
      realm.setMappingManager(securityDomainContext.getMappingManager());
      realm.setAuditManager(securityDomainContext.getAuditManager());
      context.setRealm(realm);
      context.addValve(new RestSecurityContext(path, securityDomain));
      if ("SPNEGO".equals(authMethod)) {
         context.addValve(new NegotiationAuthenticator());
      }
   }

   public String getVirtualServer() {
      return virtualServer;
   }

   public String getSecurityDomain() {
      return securityDomain;
   }

   private static void addContextApplicationParameter(Context context, String paramName, String paramValue) {
      ApplicationParameter parameter = new ApplicationParameter();
      parameter.setName(paramName);
      parameter.setValue(paramValue);
      context.addApplicationParameter(parameter);
   }

   /** {@inheritDoc} */
   @Override
   public synchronized void stop(StopContext stopContext) {
      if (callbackHandle != null) {
         callbackHandle.remove();
      }
      try {
         hostInjector.getValue().getHost().removeChild(context);
         context.stop();
      } catch (LifecycleException e) {
         ROOT_LOGGER.contextStopFailed(e);
      }
      try {
         context.destroy();
      } catch (Exception e) {
         ROOT_LOGGER.contextDestroyFailed(e);
      }
   }

   String getCacheContainerName() {
      if (!config.hasDefined(ModelKeys.CACHE_CONTAINER)) {
         return null;
      }
      return config.get(ModelKeys.CACHE_CONTAINER).asString();
   }

   /** {@inheritDoc} */
   @Override
   public synchronized Context getValue() throws IllegalStateException {
      final Context context = this.context;
      if (context == null) {
         throw new IllegalStateException();
      }
      return context;
   }

   public InjectedValue<PathManager> getPathManagerInjector() {
      return pathManagerInjector;
   }

   public InjectedValue<VirtualHost> getHostInjector() {
      return hostInjector;
   }

   public InjectedValue<EmbeddedCacheManager> getCacheManager() {
      return cacheManagerInjector;
   }

   public InjectedValue<SecurityDomainContext> getSecurityDomainContextInjector() {
      return securityDomainContextInjector;
   }

   private static class LocalInstanceManager implements InstanceManager {

      @Override
      public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException,
            InstantiationException, ClassNotFoundException {
         return Class.forName(className).newInstance();
      }

      @Override
      public Object newInstance(String fqcn, ClassLoader classLoader) throws IllegalAccessException,
            InvocationTargetException, InstantiationException, ClassNotFoundException {
         return Class.forName(fqcn, false, classLoader).newInstance();
      }

      @Override
      public Object newInstance(Class<?> c) throws IllegalAccessException, InvocationTargetException,
            InstantiationException {
         return c.newInstance();
      }

      @Override
      public void newInstance(Object o) throws IllegalAccessException, InvocationTargetException {
         throw new IllegalStateException();
      }

      @Override
      public void destroyInstance(Object o) throws IllegalAccessException, InvocationTargetException {
      }
   }
}
