/*
 *  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.synapse.commons.beanstalk.enterprise;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.commons.jmx.MBeanRegistrar;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * An Enterprise Beanstalk is used to retrieve Enterprise JavaBean (EJB) client stubs. This class
 * implements Service Locator J2EE pattern and supports stateless and stateful session beans.
 * Enterprise beanstalks can be configured in synapse.properties file. Parameters of the JNDI
 * service to be looked up for EJBs should be provided with this configuration.
 * Beanstalks cache EJB client stubs to improve efficiency by omitting excessive JNDI
 * context creation and look ups. Cache timeout for both stateless and stateful session beans could
 * be configured independently.
 */
public class EnterpriseBeanstalk {

    private static final Log log = LogFactory.getLog(EnterpriseBeanstalk.class);

    /**
     * Name of the beanstalk
     */
    private String name;

    /**
     * Properties used while initializing this beanstalk.
     */
    private Properties props;

    /**
     * Executor that periodically runs the Cleaner.
     */
    private ScheduledExecutorService scheduler;

    /**
     * Scheduled future that represents the scheduled Cleaner.
     */
    private ScheduledFuture<?> scheduledFuture;

    /**
     * Cache timeout for stateless session bean stubs.
     */
    private int statelessBeanTimeoutMinutes = 30;

    /**
     * Cache timeout for stateful session bean stubs.
     */
    private int statefulBeanTimeoutMinutes = 30;

    /**
     * Warn limit for stateless session beans. A warning is generated when more than this many of
     * stateless bean stubs are cached by this beanstalk.
     */
    private int statelessBeanWarnLimit = Short.MAX_VALUE;

    /**
     * Warn limit for stateful session beans. A warning is generated when more than this many of
     * stateful bean stubs are cached by this beanstalk.
     */
    private int statefulBeanWarnLimit = Short.MAX_VALUE;

    /**
     * JNDI context constructed with the properties provided while initializing this beanstalk.
     */
    private InitialContext initialCtx;

    /**
     * Stateless session bean stub cache.
     */
    private Map<String, CacheEntry> statelessBeans = new ConcurrentHashMap<String, CacheEntry>();

    /**
     * Stateful session bean stub cache.
     */
    private Map<String, CacheEntry> statefulBeans = new ConcurrentHashMap<String, CacheEntry>();

    /**
     * Constructs a new enterprise beanstalk with the given name and properties.
     * @param name Name of the enterprise beanstalk.
     * @param props Configuration properties. This should include properties of the JNDI service
     * to be looked up for EJBs.
     * @param scheduler ScheduledExecutorService for cleaning up timed-out stubs.
     */
    public EnterpriseBeanstalk(String name, Properties props, ScheduledExecutorService scheduler) {
        this.name = name;
        this.scheduler = scheduler;
        this.props = props;
    }

    /**
     * Initialize the current beanstalk by creating the JNDI context, registering the MBean etc.
     */
    public void init() {

        if (log.isDebugEnabled()) {
            log.debug("Initializing Beanstalk: " + name);
        }

        // Initialize the JNDI context.
        try {
            initialCtx = new InitialContext(props);
        } catch (NamingException e) {
            log.error("Could not initialize JNDI context for the Enterprise Beanstalk " +
                    "named '" + name + "'.", e);
            return;
        }

        // Read settings from the provided properties.
        if (props != null) {
            String value;

            value = props.getProperty(EnterpriseBeanstalkConstants.STATELESS_BEANS_TIMEOUT);
            if (value != null) {
                statelessBeanTimeoutMinutes = Integer.parseInt(value);
            }

            value = props.getProperty(EnterpriseBeanstalkConstants.STATEFUL_BEANS_TIMEOUT);
            if (value != null) {
                statefulBeanTimeoutMinutes = Integer.parseInt(value);
            }

            value = props.getProperty(EnterpriseBeanstalkConstants.STATELESS_BEANS_WARN_LIMIT);
            if (value != null) {
                statelessBeanWarnLimit = Integer.parseInt(value);
            }

            value = props.getProperty(EnterpriseBeanstalkConstants.STATEFUL_BEANS_WARN_LIMIT);
            if (value != null) {
                statefulBeanWarnLimit = Integer.parseInt(value);
            }
        }

        // Schedule the cleaner that removes expired beans periodically.
        int minDelay = Math.min(statelessBeanTimeoutMinutes, statefulBeanTimeoutMinutes);
        scheduledFuture = scheduler.scheduleWithFixedDelay(
                            new Runnable() {
                                @Override
                                public void run() {
                                    removeExpiredBeans();
                                }
                            }, minDelay, minDelay, TimeUnit.MINUTES);

        // Register the MBean for this beanstalk.
        MBeanRegistrar.getInstance().registerMBean(new EnterpriseBeanstalkView(this),
                EnterpriseBeanstalkConstants.ENTERPRISE_BEANSTALK_MBEAN_CATEGORY_NAME, name);

        if (log.isDebugEnabled()) {
            log.debug("Successfully initialized Beanstalk: " + name);
        }
    }

    /**
     * Destroys the beanstalk by performing clean up.
     */
    public void destroy() {

        scheduledFuture.cancel(false);

        MBeanRegistrar.getInstance().unRegisterMBean(
                EnterpriseBeanstalkConstants.ENTERPRISE_BEANSTALK_MBEAN_CATEGORY_NAME, name);
    }

    /**
     * Returns the name of this beanstalk.
     *
     * @return Name of the beanstalk.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns a client stub for the EJB with given class name, session id and jndi name. If the
     * stub is already available in cache, it is retrieved from there. Otherwise, a JNDI lookup
     * is performed with the given JNDI name.
     *
     * @param className Fully qualified name of the remote interface of the session bean.
     * @param sessionId Session id for stateful beans. null for stateless ones.
     * @param jndiName JNDI name of the EJB. null could be used if the bean is already available in
     * the cache.
     * @return Retrieved EJB client stub. null if the EJB is not found.
     */
    public Object getEnterpriseBean(String className, String sessionId, String jndiName) {
        return sessionId == null ?
            findEjb(statelessBeans, className, sessionId, jndiName, statelessBeanWarnLimit) :
            findEjb(statefulBeans, className, sessionId, jndiName, statefulBeanWarnLimit);
    }

    /**
     * Remove the specified client stub from cache. If the stub is not removed using this
     * method it will be automatically removed from the beanstalk after it times out.
     * @param className Fully qualified name of the remote interface of the session bean.
     * @param sessionId Session id for stateful session beans, null for stateless ones.
     * @return EJB client stub that was removed from the beanstalk.
     */
    public Object removeEnterpriseBean(String className, String sessionId) {
        return (sessionId == null) ?
                statelessBeans.remove(getMapKey(className, sessionId)) :
                statefulBeans.remove(getMapKey(className, sessionId));
    }

    /**
     * Removes expired stateless and stateful bean stubs from cache. Timeouts are configurable.
     */
    public void removeExpiredBeans() {
        removeExpiredBeansFromMap(statelessBeans, statelessBeanTimeoutMinutes);
        removeExpiredBeansFromMap(statefulBeans, statefulBeanTimeoutMinutes);
    }

    /**
     * Retrieves an EJB client stub from the cache, looks up in the JNDI service if it is not
     * available in the cache. If the bean is found from JNDI lookup, it is added to the cache.
     *
     * @param map Cache to search the stub in.
     * @param className Fully qualified name of the remote interface of the session bean.
     * @param sessionId Session id for stateful beans. null for stateless ones.
     * @param jndiName JNDI name of the EJB. null could be used if the bean is already available in
     * the cache.
     * @param warnLimit If this many of stubs are already available in the cache, a warning is
     * generated before adding a new stub to it.
     * @return Retrieved EJB client stub. null if the EJB is not found.
     */
    private Object findEjb(Map<String, CacheEntry> map, String className, String sessionId,
                           String jndiName, int warnLimit) {

        CacheEntry entry = map.get(getMapKey(className, sessionId));

        if (entry == null && jndiName != null) {

            synchronized (this) {
                entry = map.get(getMapKey(className, sessionId));
                if (entry == null) {
                    Object ejb = lookupInJndi(jndiName);
                    if (ejb != null) {
                        map.put(getMapKey(className, sessionId), entry = new CacheEntry(ejb));
                        int size = map.size();
                        if (size > warnLimit) {
                            String type = sessionId == null ? "stateless" : "stateful";
                            log.warn("Warn limit reached for " + type + " beans. Currently there " +
                                    "are " + size + " " + type + " EJB stubs cached in '" + name +
                                    "' " + "beanstalk.");
                        }
                    }
                }
            }

        }

        if (entry == null) {
            return null;
        } else {
            entry.markLastAccessTime();
            return entry.getBean();
        }

    }

    /**
     * Looks up the given resource in the JNDI service.
     * @param jndiName JNDI name of the resource (EJB).
     * @return Resource retrieved from the JNDI lookup. null if no resource is found.
     */
    private Object lookupInJndi(String jndiName) {
        try {
            return initialCtx.lookup(jndiName);
        } catch (NamingException ex) {
            log.error("Lookup failed for JNDI name: " + jndiName, ex);
            return null;
        }
    }

    /**
     * Remove stubs from the given map that have not been used for a time period longer than the
     * provided timeout.
     * @param map Bean stub cache.
     * @param timeoutInMinutes Expiry timeout.
     */
    private void removeExpiredBeansFromMap(Map<String, CacheEntry> map, int timeoutInMinutes) {

        Iterator<Map.Entry<String, CacheEntry>> itr = map.entrySet().iterator();

        while (itr.hasNext()) {

            Map.Entry<String, CacheEntry> mapEntry = itr.next();

            if (System.currentTimeMillis() - mapEntry.getValue().getLastAccessTime() >
                                                        timeoutInMinutes * 60L * 1000L) {

                if (log.isDebugEnabled()) {
                    log.debug("Removing the timed-out EJB stub with key '" + mapEntry.getKey() +
                            "', from '" + name + "' beanstalk cache.");
                }
                itr.remove();
            }
        }
    }

    /**
     * Derives the map key for this session bean.
     * @param className Fully qualified name of the remote interface of the EJB.
     * @param sessionId Session Id for stateful beans, null for stateless ones.
     * @return Map key derived from the given parameters.
     */
    private String getMapKey(String className, String sessionId) {
        return sessionId == null ? className : className + "-" + sessionId;
    }

    Map<String, CacheEntry> getStatelessBeans() {
        return statelessBeans;
    }

    Map<String, CacheEntry> getStatefulBeans() {
        return statefulBeans;
    }
}
