/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2008 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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.ejb21;

import java.rmi.RemoteException;
import java.util.ArrayList;

import javax.ejb.EJBException;
import javax.naming.NamingException;
import javax.transaction.SystemException;

import org.objectweb.util.monolog.api.BasicLevel;
import org.ow2.jonas.deployment.ejb.SessionDesc;
import org.ow2.util.event.api.EventPriority;
import org.ow2.util.event.api.IEvent;
import org.ow2.util.event.api.IEventListener;

/**
 * This class is a factory for a Session Bean. It is responsible for - managing
 * Home and LocalHome. - keeping the JNDI context for this component
 * (java:comp/env)
 * @author Philippe Durieux
 * @author Malek Chahine: EJB statistics
 * @author eyindanga (Refactoring of statistic event dispatcher)
 */
public abstract class JSessionFactory extends JFactory implements IEventListener {

    /**
     * Timeout for this session.
     */
    int timeout = 0;

    protected JSessionHome home = null;

    protected JSessionLocalHome localhome = null;

    protected boolean isSynchro = false;

    protected boolean isStateful; // set after constructor call

    private boolean monitoringSettingsDefinedInDD = false;

    protected boolean monitoringEnabled = false;

    protected int warningThreshold = 0;

    protected long numberOfCalls = 0;

    protected long totalBusinessProcessingTime = 0;

    protected long totalProcessingTime = 0;

    /**
     * Pool of free JSessionSwitch objects
     * Used if singleswitch=false.
     */
    protected ArrayList<JSessionSwitch> sessionList = new ArrayList<JSessionSwitch>();

    /**
     * Unique JSessionSwitch when singleswitch=true
     */
    protected JSessionSwitch uniqueSession = null;

    /**
     * If no timeout, we can manage only 1 JSessionSwitch for all sessions.
     * Only used in case of stateless, for load balancing.
     */
    protected boolean singleswitch;

    /**
     * constructor
     * @param dd The Session Deployment Descriptor
     * @param cont The Container where the bean is defined.
     */
    public JSessionFactory(final SessionDesc dd, final JContainer cont) {
        super(dd, cont);
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        txbeanmanaged = dd.isBeanManagedTransaction();
        timeout = dd.getSessionTimeout();

        // read the monitoring Settings
        monitoringEnabled = dd.isMonitoringEnabled();
        warningThreshold = dd.getWarningThreshold();
        monitoringSettingsDefinedInDD = dd.isMonitoringSettingsDefinedInDD();
        // Create the Home if defined in DD
        Class<?> homeclass = null;
        String clname = dd.getFullWrpHomeName();
        if (clname != null) {
            try {
                homeclass = cont.getClassLoader().loadClass(clname);
            } catch (ClassNotFoundException e) {
                throw new EJBException(ejbname + " Cannot load " + clname, e);
            }
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, ejbname + ": " + clname + " loaded");
            }
            try {
                // new JSessionHome(dd, this)
                int nbp = 2;
                Class<?>[] ptype = new Class<?>[nbp];
                Object[] pobj = new Object[nbp];
                ptype[0] = org.ow2.jonas.deployment.ejb.SessionDesc.class;
                pobj[0] = dd;
                ptype[1] = org.ow2.jonas.lib.ejb21.JSessionFactory.class;
                pobj[1] = this;
                home = (JSessionHome) homeclass.getConstructor(ptype).newInstance(pobj);
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot create home ", e);
            }
            // register it in JNDI
            try {
                home.register();
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot register home ", e);
            }
        }

        // Create the LocalHome if defined in DD
        clname = dd.getFullWrpLocalHomeName();
        if (clname != null) {
            try {
                homeclass = cont.getClassLoader().loadClass(clname);
            } catch (ClassNotFoundException e) {
                throw new EJBException(ejbname + " Cannot load " + clname, e);
            }
            if (TraceEjb.isDebugIc()) {
                TraceEjb.interp.log(BasicLevel.DEBUG, ejbname + ": " + clname + " loaded");
            }
            try {
                // new JSessionLocalHome(dd, this)
                int nbp = 2;
                Class<?>[] ptype = new Class<?>[nbp];
                Object[] pobj = new Object[nbp];
                ptype[0] = org.ow2.jonas.deployment.ejb.SessionDesc.class;
                pobj[0] = dd;
                ptype[1] = org.ow2.jonas.lib.ejb21.JSessionFactory.class;
                pobj[1] = this;
                localhome = (JSessionLocalHome) homeclass.getConstructor(ptype).newInstance(pobj);
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot create localhome ", e);
            }
            // register it in JNDI
            try {
                localhome.register();
            } catch (Exception e) {
                throw new EJBException(ejbname + " Cannot register localhome ", e);
            }
        }
    }

    /**
     * Return true if singleswitch option is on.
     * @return
     */
    public boolean singleSwitchOn() {
        return singleswitch;
    }

    // ---------------------------------------------------------------
    // BeanFactory implementation
    // ---------------------------------------------------------------

    /**
     * stop this EJB. Mainly unregister it in JNDI.
     */
    public void stop() {
        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }
        try {
            if (home != null) {
                home.unregister();
            }
            if (localhome != null) {
                localhome.unregister();
            }
        } catch (NamingException e) {
        }

        // Must unexport the unique session now.
        if (singleswitch && uniqueSession != null) {
            uniqueSession.noLongerUsed();
        }
        stopContainer();

    }

    /**
     * synchronize bean instances if needed
     */
    public void syncDirty(final boolean notused) {
    }

    /**
     * @return the home if exist
     */
    public JHome getHome() {
        return home;
    }

    /**
     * @return the local home if exist
     */
    public JLocalHome getLocalHome() {
        return localhome;
    }

    // ---------------------------------------------------------------
    // JSessionSwitch pool management
    // ---------------------------------------------------------------

    /**
     * Create a new Session Find one in the pool, or create a new object.
     * @return a SessionSwitch
     */
    public synchronized JSessionSwitch createEJB() throws RemoteException {

        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        if (singleswitch) {
            if (uniqueSession == null) {
                uniqueSession = createNewSession();
            }
            return uniqueSession;
        }

        JSessionSwitch bs = null;
        if (sessionList.size() > 0) {
            bs = sessionList.remove(0);
            // must re-export the remote object in the Orb since EJBObjects
            // should be un exported when put in the pool.
            JSessionRemote remote = bs.getRemote();
            if (remote != null) {
                if (remote.exportObject() == false) {
                    TraceEjb.logger.log(BasicLevel.ERROR, "bad JSessionSwitch found in pool.");
                    return null;
                }
            }
        } else {
            // This depend on subclass (stateless or stateful)
            // no need to export the EJBObject because it is basically a
            // RemoteObject
            bs = createNewSession();
        }

        // Start a timer for this new session
        if (timeout > 0) {
            bs.startTimer(timeout * 1000);
        }
        return bs;
    }

    /**
     * remove a Session. This may be called also on timeout. put it back in the
     * pool for later use.
     * @param bs The Bean Session Switch to put back in the pool.
     */
    public synchronized void removeEJB(final JSessionSwitch bs) {

        if (TraceEjb.isDebugIc()) {
            TraceEjb.interp.log(BasicLevel.DEBUG, "");
        }

        if (singleswitch) {
            uniqueSession = null;
        } else {
            sessionList.add(bs);
        }
    }

    // ---------------------------------------------------------------
    // other public methods
    // ---------------------------------------------------------------

    /**
     * Session beans can be container managed or bean managed transaction
     * Session home don't check transactional context.
     * @param rctx The Request Context
     */
    @Override
    public void checkTransaction(final RequestCtx rctx) {
        if (txbeanmanaged) {
            try {
                rctx.clientTx = tm.suspend();
                if (TraceEjb.isDebugTx()) {
                    TraceEjb.tx.log(BasicLevel.DEBUG, "suspending tx:" + rctx.clientTx);
                }
            } catch (SystemException e) {
                throw new EJBException("cannot suspend transaction", e);
            }
        } else {
            checkTransactionContainer(rctx);
        }
    }

    /**
     * @return True if this Session implements SessionSynchronization
     */
    public boolean isSessionSynchro() {
        return isSynchro;
    }

    /**
     * @return the current timeout value in seconds for Jmx
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * set the current timeout value for Jmx
     * @param timeout in seconds
     */
    public void setTimeout(final int t) {
        if (TraceEjb.isDebugTx()) {
            TraceEjb.tx.log(BasicLevel.DEBUG, "");
        }
        timeout = t;
    }

    /**
    * @return true if EJB monitoring settings have been defined in the
    *              deployment descriptor
    */
   public boolean getMonitoringSettingsDefinedInDD() {
       return this.monitoringSettingsDefinedInDD;
   }

   /**
    * @param monitoringSettingsDefinedInDD Whether EJB monitoring settings
    *                                      have been defined in the deployment
    *                                      descriptor
    */
   public void setMonitoringSettingsDefinedInDD(final boolean monitoringSettingsDefinedInDD) {
       this.monitoringSettingsDefinedInDD = monitoringSettingsDefinedInDD;
   }

   /**
    * @return true if EJB monitoring is active.
    */
   public boolean getMonitoringEnabled() {
       return this.monitoringEnabled;
   }

   /**
    * @param monitoringEnabled Whether to activate EJB monitoring.
    */
   public void setMonitoringEnabled(final boolean monitoringEnabled) {
       this.monitoringEnabled = monitoringEnabled;
   }

   /**
    * @return Number of milliseconds after which methods will start warning.
    */
   public int getWarningThreshold() {
       return this.warningThreshold;

   }

   /**
    * @param warningThreshold Number of milliseconds after which methods
    *                         will start warning.
    */
   public void setWarningThreshold(final int warningThreshold) {
       if (warningThreshold < 0) {
           throw new IllegalArgumentException("warningThreshold must be positive or 0");
       }
       this.warningThreshold = warningThreshold;
   }

   /**
    * @return Total number of calls on this EJB.
    */
   public long getNumberOfCalls() {
       return this.numberOfCalls;
   }

   /**
    * @param numberOfCalls Total number of calls on this EJB.
    */
   public void setNumberOfCalls(final long numberOfCalls) {
       this.numberOfCalls = numberOfCalls;
   }

   /**
    * @return Total time (in millis) spent in business execution.
    */
   public long getTotalBusinessProcessingTime() {
       return this.totalBusinessProcessingTime;
   }

   /**
    * @param totalBusinessProcessingTime Total time (in millis) spent in
    *                                    business execution.
    */
   public void setTotalBusinessProcessingTime(final long totalBusinessProcessingTime) {
       this.totalBusinessProcessingTime = totalBusinessProcessingTime;
   }

   /**
    * @return Total time (in millis) spent in business + container execution.
    */
   public long getTotalProcessingTime() {
       return this.totalProcessingTime;
   }

   /**
    * @param totalProcessingTime Total time (in millis) spent in business +
    *                            container execution.
    */
   public void setTotalProcessingTime(final long totalProcessingTime) {
       this.totalProcessingTime = totalProcessingTime;
   }

    /**
     * @return true if this Session is Stateful. Will be used internally by
     *         EJBObject or EJBLocalObject because they are common to both
     *         types.
     */
    public boolean isStateful() {
        return isStateful;
    }

    public boolean accept(final IEvent event) {
        if (event instanceof MonitoringEvent) {
            MonitoringEvent mEvent = (MonitoringEvent) event;
            return mEvent.getBeanFactoryId() == System.identityHashCode(this);
        } else {
            return false;
        }
    }

    public EventPriority getPriority() {
        return EventPriority.ASYNC_LOW;
    }

    public void handle(final IEvent event) {
        MonitoringEvent mEvent = (MonitoringEvent) event;
        long totalProcessingTime = mEvent.getTotalStopTime() - mEvent.getTotalStartTime();
        long businessProcessingTime = mEvent.getBusinessStopTime() - mEvent.getBusinessStartTime();

        synchronized(this) {
            this.numberOfCalls++;
            this.totalProcessingTime += totalProcessingTime;
            this.totalBusinessProcessingTime += businessProcessingTime;
        }

        if (this.getWarningThreshold() > 0 && totalProcessingTime > this.getWarningThreshold()) {
            TraceEjb.interp.log(BasicLevel.WARN, "EJB processing time exceeds threshold :\n method=" + mEvent.getMethodName()
                + ", time=" + totalProcessingTime + " millis, threshold=" + this.getWarningThreshold() + " millis.");
        }
    }

    /**
     * @return a JSessionContext
     */
    public abstract JSessionContext getJContext(JSessionSwitch ss);

    // ---------------------------------------------------------------
    // private methods
    // ---------------------------------------------------------------

    abstract protected JSessionSwitch createNewSession() throws RemoteException;
}
