/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library 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 library 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.multitenant.internal;

import javax.servlet.Filter;

import org.objectweb.util.monolog.api.LogInfo;
import org.ow2.carol.jndi.intercept.manager.SingletonInterceptorManager;
import org.ow2.easybeans.api.EZBServer;
import org.ow2.easybeans.persistence.api.EZBPersistenceUnitManager;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.service.AbsServiceImpl;
import org.ow2.jonas.lib.tenant.TenantIdInfo;
import org.ow2.jonas.lib.tenant.context.TenantCurrent;
import org.ow2.jonas.lib.tenant.filter.HttpTenantIdFilter;
import org.ow2.jonas.lib.tenant.context.TenantContext;
import org.ow2.jonas.lib.tenant.interceptor.jmx.JMXTenantIdInterceptor;
import org.ow2.jonas.lib.tenant.interceptor.jndi.JNDITenantIdInterceptor;
import org.ow2.jonas.lib.tenant.listener.TenantEventListener;
import org.ow2.jonas.lib.util.JonasObjectName;
import org.ow2.jonas.multitenant.MultitenantService;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.service.ServiceException;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.ee.deploy.api.deployable.IDeployableInfo;
import org.ow2.util.event.api.IEventService;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;


/**
 * Implements the multitenant service.
 * @see org.ow2.jonas.multitenant.MultitenantService
 *
 * @author Mohammed Boukada
 */
public class MultitenantServiceImpl extends AbsServiceImpl implements MultitenantService, LogInfo {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(MultitenantServiceImpl.class);

    /**
     * The JMX server.
     */
    private static JmxService jmxService = null;

    /**
     * Registry service.
     */
    private static RegistryService registryService = null;

    /**
     * Default deployment policy to use when deploying a new version of a
     * versioned application, can be changed via JMX.
     */
    private String defaultPolicy = "Reserved";

    /**
     * Monolog pattern
     */
    private char pattern = 'T';

    /**
     * Tenant Id interceptor for MBeans
     */
    private JMXTenantIdInterceptor jmxTenantIdInterceptor = null;

    /**
     * Tenant Id attribute name (for MBeans)
     */
    private String tenantIdAttributeName = "tenant-id";

    /**
     * Whether tenants have access to platform MBeans
     */
    private boolean allowToAccessPlatformMBeans = true;

    /**
     * Tenant Id interceptor for JNDI names
     */
    private JNDITenantIdInterceptor jndiTenantIdInterceptor = null;

    /**
     * Separator between tenant-id prefix and JNDI name
     */
    private final String JNDI_SEPARATOR = "/";

    /**
     * The EventService.
     */
    private IEventService eventService = null;

    /**
     * Event Listener
     */
    private TenantEventListener eventListener = null;

    /**
     * @return Default policy, as set via JMX.
     */
    public String getDefaultDeploymentPolicy() {
        return defaultPolicy;
    }

    /**
     * @param jmxService JMX service to set.
     */
    public void setJmxService(final JmxService jmxService) {
        this.jmxService = jmxService;
    }

    /**
     * @param registryService Registry service to set.
     */
    public void setRegistryService(final RegistryService registryService) {
        this.registryService = registryService;
    }

    /**
     * @param eventService the EventService to set
     */
    public void setEventService(final IEventService eventService) {
        this.eventService = eventService;
    }

    @Override
    protected void doStart() throws ServiceException {
        MultitenantVirtualJNDIBinding.initialize(jmxService, registryService, this);

        // Load mbeans-descriptor.xml into the modeler registry
        // the meta-data located in META-INF/mbeans-descriptors.xml
        jmxService.loadDescriptors(getClass().getPackage().getName(), getClass().getClassLoader());

        try {
            jmxService.registerModelMBean(this, JonasObjectName.multitenant(getDomainName()));
        } catch (Exception e) {
            throw new ServiceException("Cannot register 'multitenant' service MBean", e);
        }
        logger.info("Multitenant service management bean has been registered successfully");

        // Add tenantId Event Listener
        eventListener = new TenantEventListener(JNDI_SEPARATOR);
        eventService.registerListener(eventListener, EZBServer.NAMING_EXTENSION_POINT);
        eventService.registerListener(eventListener, "/easybeans/.*");

        // Add tenantId JMX interceptor
        jmxTenantIdInterceptor = new JMXTenantIdInterceptor(tenantIdAttributeName, allowToAccessPlatformMBeans);
        jmxService.addInterceptor(jmxTenantIdInterceptor);

        // Add tenantId JNDI interceptor
        jndiTenantIdInterceptor = new JNDITenantIdInterceptor(JNDI_SEPARATOR);
        SingletonInterceptorManager.getInterceptorManager().registerContextInterceptor(jndiTenantIdInterceptor);
    }

    @Override
    protected void doStop() throws ServiceException {
        jmxService.removeInterceptor(jmxTenantIdInterceptor);
        SingletonInterceptorManager.getInterceptorManager().unregisterContextInterceptor(jndiTenantIdInterceptor);
        eventService.unregisterListener(eventListener);
        jmxService.unregisterMBean(JonasObjectName.multitenant(getDomainName()));
        logger.info("Multitenant service management bean has been unregistered successfully");
    }

    /**
     * Creates an instance of a TenantId HTTP Filter
     * @param  tenantId the tenant identifier
     * @return an instance of a TenantId HTTP Filter
     */
    public Filter getTenantIdFilter(String tenantId){
        return new HttpTenantIdFilter(tenantId);
    }

    /**
     * Returns the default tenantId value
     * @return the default tenantId value
     */
    public String getDefaultTenantID(){
        return TenantContext.DEFAULT_TENANT_ID;
    }

    /**
     * Creates JNDI binding management beans for a given tenant identifier.
     * @param deployable JAR, WAR or EAR object.
     * @param tenantId tenant identifier of the application which will prefix JNDI names.
     */
    public void createJNDIBindingMBeans(final IDeployable<?> deployable, final String tenantId) {

        if (getDefaultTenantID().equals(tenantId)) {
            logger.warn("This application has not a specific tenant identifier. Default tenant identifier is used");
        }

        if (tenantId != null) {
            String applicationName = getOriginalDeployable(deployable).getModuleName();
            String prefix = tenantId;
            MultitenantVirtualJNDIBinding.createJNDIBindingMBeans(applicationName, prefix);
        }
    }

    /**
     * @param deployable Deployable to get the original deployable from
     *        (recursive).
     * @return Original deployable.
     */
    private IDeployable<?> getOriginalDeployable(final IDeployable<?> deployable) {
        IDeployable<?> originalDeployable = deployable;
        while (originalDeployable.getOriginalDeployable() != null) {
            originalDeployable = originalDeployable.getOriginalDeployable();
        }
        return originalDeployable;
    }

    /**
     * Removes JNDI binding management beans that are not in the JNDI directory
     * anymore.
     */
    public void garbageCollectJNDIBindingMBeans() {
        MultitenantVirtualJNDIBinding.garbageCollectJNDIBindingMBeans();
    }

    /**
     * Add eclipselink properties
     * @param persistenceUnitManager persistence unit manager
     * @param tenantId tenant identifier
     */
    public void updatePersistenceUnitManager(EZBPersistenceUnitManager persistenceUnitManager, String tenantId){
        // This property will configure entities as multitenant
        // It is the equivalent of @Multitenant
        String sessionCustomizerProperty = "eclipselink.session.customizer";
        String sessionCustomizerClass = "org.ow2.easybeans.persistence.eclipselink.MultitenantEntitiesSessionCustomizer";
        persistenceUnitManager.setProperty(sessionCustomizerProperty, sessionCustomizerClass);

        // If eclipselink was enabled to drop and create tables
        // change this property to only create tables
        String createTablesProperty = "eclipselink.ddl-generation";
        String dropAndCreateTablesValue = "drop-and-create-tables";
        String createTablesValue = "create-tables";
        Map<String, String> properties = persistenceUnitManager.getProperty(createTablesProperty);
        for (Map.Entry<String, String> property : properties.entrySet()){
            if (property.getValue().equals(dropAndCreateTablesValue)) {
                logger.warn("This tenant was enabled to drop and create tables. Eclipselink property is changed to only create tables");
                persistenceUnitManager.setProperty(createTablesProperty, createTablesValue, property.getKey());
            }
        }

        // This property will propagate the tenantId to eclipselink
        // This value will be added to entities tables
        String tenantIdProperty = "eclipselink.tenant-id";
        String tenantIdValue = tenantId;
        persistenceUnitManager.setProperty(tenantIdProperty, tenantIdValue);

        logger.debug("Tenant id ''{0}'' was propagate to eclipselink", tenantId);
    }

    /**
     * Add tenantId as extension to the earDeployable
     * @param deployable application deployable
     * @param tenantId tenant identifier to add
     */
    public void addTenantIdDeployableInfo(IDeployable deployable, String tenantId) {
        TenantIdInfo tenantIdInfo = new TenantIdInfo();
        tenantIdInfo.setTenantIdInfo(tenantId);
        deployable.addExtension(tenantIdInfo);
    }

    /**
     * Gets tenantIdInfo stored in the deployable
     * @param deployable application deployable
     * @return tenantIdInfo
     */
    public String getTenantIdDeployableInfo(IDeployable deployable) {
        if (deployable == null ) {
            logger.debug("Deployable is null");
            return null;
        }

        IDeployableInfo tenantIdInfo = deployable.getExtension(TenantIdInfo.class);
        String tenantId = null;
        try {
            if (tenantIdInfo != null){
                tenantId = (String) tenantIdInfo.getClass().getDeclaredMethod("getTenantIdInfo").invoke(tenantIdInfo);
            } else {
                logger.debug("TenantIdInfo was not found in ''{0}''", deployable);
            }
        } catch (NoSuchMethodException e) {
            throw new ServiceException("Method was not found", e);
        } catch (InvocationTargetException e) {
            throw new ServiceException("Invocation method fail ",e);
        } catch (IllegalAccessException e) {
            throw new ServiceException("Method access fail", e);
        }
        return tenantId;
    }

    /**
     * Tell if an application is multitenant
     * @param deployable application deployable
     * @return is multitenant, or not.
     */
    public boolean isMultitenant (IDeployable deployable) {
        String tenantId = this.getTenantIdDeployableInfo(deployable);
        // if tenantId is null or equal to defaultTenantId, then application
        // is not multitenant.
        return !(tenantId == null || tenantId.equals(this.getDefaultTenantID()));
    }

    /**
     * Gets tenantContext from TenantCurrent
     * @return tenantContext
     */
    public Object getTenantContext() {
        return TenantCurrent.getCurrent().getTenantContext();
    }

    /**
     * Gets tenant id from TenantContext
     * @return tenant id
     */
    public String getTenantIdFromContext() {
        if (TenantCurrent.getCurrent().getTenantContext() != null){
            return TenantCurrent.getCurrent().getTenantContext().getTenantId();
        }
        return null;
    }

    /**
     * Set tenant id in the TenantContext
     * @param tenantId to add in TenantContext
     */
    public void setTenantIdInContext (String tenantId) {
        TenantContext tmp = new TenantContext(tenantId);
        this.setTenantContext(tmp);
    }

    /**
     * Set TenantContext in the Thread
     * @param ctx
     */
    public void setTenantContext(Object ctx) {
        TenantCurrent.getCurrent().setTenantContext((TenantContext) ctx);
    }

    /**
     * Set application instance name in TenantContext
     * @param instanceName
     */
    public void setInstanceNameInContext(String instanceName) {
        String tenantId = null;
        String jmxSessionTenantId = null;
        // if tenantId and JMX Session tenantId are set, set them
        // in the new TenantContext to be propagate
        if (TenantCurrent.getCurrent().getTenantContext() != null) {
            tenantId = TenantCurrent.getCurrent().getTenantContext().getTenantId();
            jmxSessionTenantId = TenantCurrent.getCurrent().getTenantContext().getJmxSessionTenantId();
        }
        TenantContext tmp = new TenantContext(tenantId, jmxSessionTenantId, instanceName);
        this.setTenantContext(tmp);
    }

    /**
     * Get application instance name from TenantContext
     * @return
     */
    public String getInstanceNameFromContext() {
        if (TenantCurrent.getCurrent().getTenantContext() != null) {
            return TenantCurrent.getCurrent().getTenantContext().getInstanceName();
        }
        return null;
    }

    /**
     * Set TenantId and Instance Name in TenantContext
     * @param tenantId
     * @param instanceName
     */
    public void setTenantIdAndInstanceNameInContext(String tenantId, String instanceName) {
        TenantContext tmp = new TenantContext(tenantId, null, instanceName);
        this.setTenantContext(tmp);
    }

    /********************************************************************
     *              LogInfo impl
     ********************************************************************/

    /**
     * Gets log info value
     * @return log info value
     */
    public String getValue() {
        String tenantId = null;

        if (TenantCurrent.getCurrent().getTenantContext() != null) {
            tenantId = TenantCurrent.getCurrent().getTenantContext().getTenantId();
        }

        if (tenantId == null) {
            return "";
        }
        return tenantId;
    }
}
