/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * 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.jorm;

import org.objectweb.jorm.api.PBinding;
import org.objectweb.jorm.api.PClassMapping;
import org.objectweb.jorm.api.PClassMappingCtrl;
import org.objectweb.jorm.api.PException;
import org.objectweb.jorm.api.PMapCluster;
import org.objectweb.jorm.api.PMapper;
import org.objectweb.jorm.api.PMappingCallback;
import org.objectweb.jorm.facility.naming.basidir.BasidBinder;
import org.objectweb.jorm.naming.api.PBinder;
import org.objectweb.jorm.naming.api.PExceptionNaming;
import org.objectweb.jorm.naming.api.PName;
import org.objectweb.jorm.naming.api.PNameCoder;
import org.objectweb.jorm.naming.api.PNamingContext;
import org.objectweb.util.monolog.api.BasicLevel;

import javax.ejb.EJBException;
import javax.ejb.EntityBean;

import org.ow2.jonas.deployment.ejb.EjbRelationshipRoleDesc;
import org.ow2.jonas.deployment.ejb.EntityCmp2Desc;
import org.ow2.jonas.deployment.ejb.EntityDesc;
import org.ow2.jonas.lib.ejb21.JContainer;
import org.ow2.jonas.lib.ejb21.JEntityFactory;
import org.ow2.jonas.lib.ejb21.JEntitySwitch;
import org.ow2.jonas.lib.ejb21.TraceEjb;

import java.io.Serializable;
import java.util.Iterator;

/**
 * This class is an extension of the JEntityFactory class. It initializes the
 * persitant class (the bean) in the jorm mapper. This class is abstract in
 * order to be extended by the PClassMapping generated for the Bean.
 *
 * @author Sebastien Chassande-Barrioz
 */
public abstract class JormFactory extends JEntityFactory implements PClassMapping, PClassMappingCtrl {

    protected int relNonInit;
    protected boolean mapped;
    protected PMapper mapper = null;
    protected EntityCmp2Desc ecd = null;

    /**
     * Class used for EntitySwitch (binding class)
    */
    private Class bindingClass = null;



    /**
     * constructor
     */
    public JormFactory() {
        super();
    }

    protected abstract void setMapper(String mapperName) throws PException;
    public abstract Object getConnection(Object hints) throws PException;
    public abstract void releaseConnection(Object conn) throws PException;

    /**
     * Initialization of the factory. This is called just after the newInstance()
     * from the JContainer (addBean method)
     */
    public void init(EntityDesc ed, JContainer c, String mapperName) {
        if (TraceEjb.isDebugFactory()) {
            TraceEjb.factory.log(BasicLevel.DEBUG, ed.getEjbName());
        }

        // First, call the super in order to have the access to the datasource
        // and the beanNaming
        super.init(ed, c);
        ecd = (EntityCmp2Desc) ed;
        try {
            setMapper(mapperName);
        } catch (PException e) {
            throw new EJBException("JormFactory cannot create the mapper " + mapperName, e);
        }

        //----------------------- JORM INITIALIZATION -----------------------//
        if (TraceEjb.isDebugFactory()) {
            TraceEjb.factory.log(BasicLevel.DEBUG, "Jorm initialisation");
        }
        PBinder binder = null;
        Class binderClass = null;
        int binderCT = 0;
        try {
            // Instanciate the binder
            binderClass = c.getClassLoader().loadClass(ecd.getJormBinderClassName());
            binder = (PBinder) binderClass.newInstance();
            if (ecd.hasPrimaryKeyField()) {
                binderCT = JormType.getCodingType(ecd.getCmpFieldDesc(ecd.getPrimaryKeyFieldName()).getFieldType(), true);
                ((BasidBinder) binder).setCodingType(binderCT);
            } else {
                // TODO:
                // - take the initial values of the primary key
                // - put them into the generated binder (use a PNG)
            }
            if (TraceEjb.isDebugFactory()) {
                TraceEjb.factory.log(BasicLevel.DEBUG, "binder " + ecd.getJormBinderClassName() + "instanciated");
            }

            // Link the binder and the PClassMapping
            binder.setPClassMapping(this);
            setPBinder(binder);
            setClassPNameCoder(binder);
            if (TraceEjb.isDebugFactory()) {
                TraceEjb.factory.log(BasicLevel.DEBUG, "binder linked to the mapping");
            }

        } catch (Exception e) {
            TraceEjb.factory.log(BasicLevel.ERROR, "Impossible to create the binder", e);
            throw new EJBException("Impossible to create the binder: bean:" + ecd.getEjbName(), e);
        }

        // for each reference assignes the PNamingcontext if it is possible.
        relNonInit = 0;
        try {
            for (Iterator it = ecd.getEjbRelationshipRoleDescIterator(); it.hasNext();) {
                EjbRelationshipRoleDesc rsr = (EjbRelationshipRoleDesc) it.next();
                String source = rsr.getSourceBean().getEjbName();
                String target = rsr.getTargetBean().getEjbName();
                JormFactory pcm2 = source.equals(target) ? this : (JormFactory) c.getBeanFactory(target);
                if (TraceEjb.isDebugFactory()) {
                    TraceEjb.factory.log(BasicLevel.DEBUG,
                            "treatement of the relation " + rsr.getRelation().getName()
                            + ": current-bean=" + ecd.getEjbName()
                            + ", source-bean=" + source
                            + ", dest-bean=" + target
                            + ", cmr-field=" + rsr.getCmrFieldName());
                }
                if (rsr.hasCmrField()) {
                    PClassMapping gcm = null;
                    //Multivalued relation
                    if (rsr.isTargetMultiple()) {
                        // Instanciate a GenClassMapping and link it
                        gcm = newGCMInstance(mapperName);
                        gcm.init((PMappingCallback) mapper, null);
                        setGenClassMapping(rsr.getCmrFieldName(), gcm);
                        if (TraceEjb.isDebugFactory()) {
                            TraceEjb.factory.log(BasicLevel.DEBUG,
                                    "assign a GenClassMapping for the CMR "
                                    + rsr.getCmrFieldName() + " / gcm=" + gcm);
                        }

                        PBinder gcmBinder = null;
                        try {
                            gcmBinder = (PBinder) binderClass.newInstance();
                        } catch (Exception e) {
                            TraceEjb.factory.log(BasicLevel.ERROR,
                                                "Impossible to create the binder of the GCM bean: "
                                                + ecd.getEjbName() + " / CMR: "
                                                + rsr.getCmrFieldName(), e);
                            throw new EJBException(
                                                   "Impossible to create the binder of the GCM bean: "
                                                   + ecd.getEjbName() + " / CMR: "
                                                   + rsr.getCmrFieldName(), e);
                        }
                        if (ecd.hasPrimaryKeyField()) {
                            ((BasidBinder) gcmBinder).setCodingType(binderCT);
                        } else {
                            // TODO: (not so important)
                            // - take the initial values of the primary key
                            // - put them into the generated binder (use a PNG)
                        }
                        gcm.setPBinder(gcmBinder);
                        gcmBinder.setPClassMapping(gcm);
                        setPNameCoder(rsr.getCmrFieldName(), (PNameCoder) gcmBinder);
                    }

                    // As the bean with which the current bean is in relation
                    // may be not load, the test of pcm2!=null is needed
                    if (pcm2 != null) {
                        if (TraceEjb.isDebugFactory()) {
                            TraceEjb.factory.log(BasicLevel.DEBUG, "Pnc Assignement");
                        }
                        if (rsr.isTargetMultiple()) {
                            ((PClassMappingCtrl)gcm).setPNameCoder((PNameCoder) pcm2.getPBinder());
                        } else {
                            setPNameCoder(rsr.getCmrFieldName(), (PNameCoder) pcm2.getPBinder());
                        }
                        if (pcm2.isPrefetch()) {
                            initGenClassPrefetch(gcm, pcm2);
                        }
                    } else {
                        relNonInit++;
                        if (TraceEjb.isDebugFactory()) {
                            TraceEjb.factory.log(BasicLevel.DEBUG, "the Pnc is not reachable currently. relNonInit=" + relNonInit);
                        }
                        // The PNamingContext has not been set.
                    }
                }
                EjbRelationshipRoleDesc rsr2 = rsr.getOppositeRelationshipRole();
                if (pcm2 != null && rsr2.hasCmrField() && pcm2 != this) {
                    // This case appears when the scheduling of the bean adding
                    // in the container is bad (or cyclic relation): A bean has
                    // been load and have a relation to current bean. We must
                    // therfore set the naming context on the PClassMapping of
                    // the first bean for the opposite CMR field
                    if (TraceEjb.isDebugFactory()) {
                        TraceEjb.factory.log(BasicLevel.DEBUG,
                                "later Pnc assignement of the opposite CMR field: "
                                + rsr2.getCmrFieldName());
                    }
                    pcm2.configurePnc(rsr2.getCmrFieldName(), (PNamingContext) getPBinder(), this, rsr2.isTargetMultiple());
                }
            }
        } catch (EJBException e) {
            throw e;
        } catch (Exception e) {
            TraceEjb.factory.log(BasicLevel.ERROR,
                                "Impossible to assign the naming context to the PClassMapping", e);
            throw new EJBException("Impossible to assign the naming context to the PClassMapping", e);
        }
        mapped = false;
        if (relNonInit == 0) {
            mapClass();
        }

        // load the binding class
        String cn = ((EntityCmp2Desc) dd).getJormBindingClassName();
        try {
            bindingClass = getContainer().getClassLoader().loadClass(cn);
        } catch (ClassNotFoundException e) {
            String err = "Impossible to load binding class '" + cn + "'.";
            TraceEjb.factory.log(BasicLevel.ERROR, err, e);
            throw new EJBException(err, e);
        }

    }

    /**
     * @return mapper
     */
    public PMapper getMapper() {
        return mapper;
    }

    /**
     * It assignes the PNamingContext which manages a relation.
     * @param n
     * @param pnc
     * @param isMultiple
     */
    public void configurePnc(String n, PNamingContext pnc, JormFactory target, boolean isMultiple) throws PException {
        if (isMultiple) {
            PClassMappingCtrl gcm = (PClassMappingCtrl) getGenClassMapping(n);
            gcm.setPNameCoder(pnc);
            if (target.isPrefetch()) {
                //Initializes the genclass prefetching too
                initGenClassPrefetch((PClassMapping) gcm, target);
            }

        } else {
            setPNameCoder(n, pnc);
        }
        relNonInit--;
        if (TraceEjb.isDebugFactory()) {
            TraceEjb.factory.log(BasicLevel.DEBUG, "PNamingContext assigned, relNonInit=" + relNonInit);
        }
        if (relNonInit == 0) {
            mapClass();
        }
    }

    private void mapClass() {
        if (mapped) {
            throw new EJBException("The class is already mapped");
        }

        try {
            // Map the user class into the mapper
            mapper.map(this);
            // Drop or Create tables, depending on what is in the DD
            PMapCluster pmapclust = mapper.getPMapCluster(getClassName());
            if (pmapclust.isDefined()) {
                switch (ecd.getCleanupPolicy()) {
                case EntityDesc.CLEANUP_REMOVEDATA:
                    pmapclust.createMappingStructures(false);
                    pmapclust.deleteData();
                    break;
                case EntityDesc.CLEANUP_REMOVEALL:
                    pmapclust.deleteMappingStructures();
                    pmapclust.createMappingStructures(true);
                    break;
                case EntityDesc.CLEANUP_NONE:
                    break;
                case EntityDesc.CLEANUP_CREATE:
                    pmapclust.createMappingStructures(false);
                    break;
                default:
                    throw new EJBException("Unknown cleanup policy: " + ecd.getCleanupPolicy());
                }
            } else {
                // nothing in the JOnAS case because that means several beans have
                // common structure due to the CMR. Then the last element of the
                // PMapCluster should really apply your data action.
            }
            mapped = true;
            if (TraceEjb.isDebugFactory()) {
                TraceEjb.factory.log(BasicLevel.DEBUG, getClassName() + " is mapped");
            }
        } catch (PException pe) {
            Exception e = pe;
            while ((e instanceof PException) && ((PException) e).getNestedException() != null) {
                e = ((PException) e).getNestedException();
            }
            TraceEjb.factory.log(BasicLevel.ERROR, "Impossible to map the class on the rdb mapper", e);
            throw new EJBException("Impossible to map the class on the rdb mapper", e);
        }
    }

    public void stop() {
        super.stop();
        try {
            mapper.unmap(getClassName());
            mapped = false;
        } catch (PException e) {
            TraceEjb.factory.log(BasicLevel.ERROR,
                                "Impossible to unmap the class " + getClassName(), e);
        }
    }

    /**
     * This method is overrided in order to specify the JEntityContext class
     * which must be instanciated.
     * Create a new instance of the bean and its EntityContext
     * In case of CMP, the bean class is derived to manage entity persistence.
     */
    protected org.ow2.jonas.lib.ejb21.JEntityContext createNewContext(EntityBean bean) {
        return new org.ow2.jonas.lib.ejb21.jorm.JEntityContext(this, bean);
    }

    public JEntitySwitch getJEntitySwitch() {
        Object result = null;
        try {
            result = bindingClass.newInstance();
        } catch (Exception e) {
            TraceEjb.factory.log(BasicLevel.ERROR, "Impossible to create a new JEntitySwitch as specified in BeanNaming: " + bindingClass.getName(), e);
            return super.getJEntitySwitch();
        }
        try {
            ((PBinding) result).init(this);
        } catch (PException e) {
            TraceEjb.factory.log(BasicLevel.ERROR, "Impossible to initialized the new JEntitySwitch as specified in BeanNaming: " + bindingClass.getName(), e);
        }
        return (JEntitySwitch) result;
    }

    /**
     * Create a GenClassMapping
     * @param mapperName name of the mapper
     */
    protected PClassMapping newGCMInstance(String mapperName) throws Exception {
        int idx = mapperName.indexOf(".");
        String mn = (idx != -1) ? mapperName.substring(0, idx) : mapperName;
        return (PClassMapping) Class.forName("org.objectweb.jorm.mapper." + mn + ".genclass."
                                             + Character.toUpperCase(mn.charAt(0))
                                             + mn.substring(1, mn.length()) + "GenClassMapping"
                                             ).newInstance();
    }

    /**
     * Encode PK in case of CMP2
     * @return String representation of the PName
     */
    public Serializable encodePK(Serializable pk) {
        try {
            return (Serializable) ((PName) pk).encodeString();
        } catch (PExceptionNaming e) {
            TraceEjb.factory.log(BasicLevel.ERROR, "impossible to serialize PK" + e);
            return pk;
        }
    }

    /**
     * Decode String to a PName
     * @return PName matching the String
     */
    public Serializable decodePK(Serializable strpk) {
        try {
            return getPBinder().decodeString((String) strpk);
        } catch (PExceptionNaming e) {
            TraceEjb.factory.log(BasicLevel.ERROR, "impossible to deserialize PK" + e);
            return strpk;
        }
    }

    /**
     * It initializes the prefetching of a genclassMapping with the
     * PClassMapping of the target class of the multivalued CMR.
     * @param gcm is the GenClassMapping to initialized
     * @param targetPCM is the PClassMapping of the target class
     */
    protected abstract void initGenClassPrefetch(PClassMapping gcm, PClassMapping targetPCM);
}
