/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ejb.plugins.cmp.jdbc;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.ejb.CreateException;
import javax.management.MalformedObjectNameException;
import javax.sql.DataSource;

import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCEntityCommandMetaData;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.security.AuthenticationManager;

/**
 * Base class for create commands that drives the operation sequence.
 *
 * @author <a href="mailto:jeremy@boynes.com">Jeremy Boynes</a>
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 */
public abstract class JDBCAbstractCreateCommand implements JDBCCreateCommand
{
   protected Logger log;
   protected boolean debug;
   protected boolean trace;
   protected JDBCEntityBridge entity;
   protected AuthenticationManager securityManager;
   protected boolean createAllowed;
   protected SQLExceptionProcessorMBean exceptionProcessor;
   protected String insertSQL;
   protected JDBCFieldBridge[] insertFields;
   protected boolean insertAfterEjbPostCreate;

   // Generated fields
   private JDBCCMPFieldBridge createdPrincipal;
   private JDBCCMPFieldBridge createdTime;
   private JDBCCMPFieldBridge updatedPrincipal;
   private JDBCCMPFieldBridge updatedTime;

   public void init(JDBCStoreManager manager) throws DeploymentException
   {
      log = Logger.getLogger(getClass().getName() + '.' + manager.getMetaData().getName());
      debug = log.isDebugEnabled();
      trace = log.isTraceEnabled();

      entity = (JDBCEntityBridge) manager.getEntityBridge();
      securityManager = manager.getContainer().getSecurityManager();

      insertAfterEjbPostCreate = manager.getContainer()
         .getBeanMetaData().getContainerConfiguration().isInsertAfterEjbPostCreate();

      // set create allowed
      createAllowed = true;
      JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
      for(int i = 0; i < pkFields.length; i++)
      {
         if(pkFields[i].isReadOnly())
         {
            createAllowed = false;
            log.debug("Create will not be allowed because pk field "
               + pkFields[i].getFieldName() + "is read only.");
            break;
         }
      }

      initGeneratedFields();

      JDBCEntityCommandMetaData entityCommand = manager.getMetaData().getEntityCommand();
      if(entityCommand == null)
      {
         throw new DeploymentException("entity-command is null");
      }
      initEntityCommand(entityCommand);

      initInsertFields();
      initInsertSQL();
   }

   protected void initEntityCommand(JDBCEntityCommandMetaData entityCommand) throws DeploymentException
   {
      String objectName = entityCommand.getAttribute("SQLExceptionProcessor");
      if(objectName != null)
      {
         try
         {
            exceptionProcessor = (SQLExceptionProcessorMBean) MBeanProxyExt.create(SQLExceptionProcessorMBean.class, objectName);
         }
         catch(MalformedObjectNameException e)
         {
            throw new DeploymentException("Invalid object name for SQLExceptionProcessor: ", e);
         }
      }
   }

   public Object execute(Method m, Object[] args, EntityEnterpriseContext ctx) throws CreateException
   {
      // TODO: implement this logic nicer
      if(insertAfterEjbPostCreate)
      {
         if(!JDBCEntityBridge.isEjbCreateDone(ctx))
         {
            checkCreateAllowed();
            generateFields(ctx);
            JDBCEntityBridge.setEjbCreateDone(ctx);
         }
         else
         {
            beforeInsert(ctx);
            performInsert(ctx);
            afterInsert(ctx);
            JDBCEntityBridge.setCreated(ctx);
         }
      }
      else
      {
         checkCreateAllowed();
         generateFields(ctx);
         beforeInsert(ctx);
         performInsert(ctx);
         afterInsert(ctx);
         JDBCEntityBridge.setCreated(ctx);
      }
      return getPrimaryKey(ctx);
   }

   protected void checkCreateAllowed() throws CreateException
   {
      if(!createAllowed)
      {
         throw new CreateException("Creation is not allowed because a primary key field is read only.");
      }
   }

   protected JDBCCMPFieldBridge getGeneratedPKField() throws DeploymentException
   {
      // extract the pk field to be generated
      JDBCCMPFieldBridge pkField = null;
      JDBCFieldBridge[] pkFields = entity.getPrimaryKeyFields();
      for(int i = 0; i < pkFields.length; ++i)
      {
         if(pkField != null)
            throw new DeploymentException("Generation only supported with single PK field");
         pkField = (JDBCCMPFieldBridge)pkFields[i];
      }
      return pkField;
   }

   protected void initGeneratedFields() throws DeploymentException
   {
      createdPrincipal = entity.getCreatedPrincipalField();
      if(securityManager == null && createdPrincipal != null)
      {
         throw new DeploymentException("No security-domain configured but created-by specified");
      }
      updatedPrincipal = entity.getUpdatedPrincipalField();
      if(securityManager == null && updatedPrincipal != null)
      {
         throw new DeploymentException("No security-domain configured but updated-by specified");
      }
      createdTime = entity.getCreatedTimeField();
      updatedTime = entity.getUpdatedTimeField();
   }

   protected void generateFields(EntityEnterpriseContext ctx) throws CreateException
   {
      // Audit principal fields
      if(securityManager != null)
      {
         String principalName = ctx.getEJBContext().getCallerPrincipal().getName();
         if(createdPrincipal != null && createdPrincipal.getInstanceValue(ctx) == null)
         {
            createdPrincipal.setInstanceValue(ctx, principalName);
         }
         /*
         if(updatedPrincipal != null && updatedPrincipal.getInstanceValue(ctx) == null)
         {
            updatedPrincipal.setInstanceValue(ctx, principalName);
         }
         */
      }

      // Audit time fields
      Date date = null;
      if(createdTime != null && createdTime.getInstanceValue(ctx) == null)
      {
         date = new Date();
         createdTime.setInstanceValue(ctx, date);
      }
      /*
      if(updatedTime != null && updatedTime.getInstanceValue(ctx) == null)
      {
         if(date == null)
            date = new Date();
         updatedTime.setInstanceValue(ctx, date);
      }
      */
   }

   protected void initInsertFields()
   {
      JDBCFieldBridge[] fields = entity.getTableFields();
      List insertFieldsList = new ArrayList(fields.length);
      for(int i = 0; i < fields.length; i++)
      {
         JDBCFieldBridge field = fields[i];
         if(isInsertField(field))
            insertFieldsList.add(field);
      }

      insertFields = (JDBCFieldBridge[]) insertFieldsList.toArray(new JDBCFieldBridge[insertFieldsList.size()]);
   }

   protected boolean isInsertField(JDBCFieldBridge field)
   {
      boolean result =
         !(field instanceof JDBCCMRFieldBridge)
         && field.getJDBCType() != null
         && !field.isReadOnly();
      if(field instanceof JDBCCMPFieldBridge)
         result = result && !((JDBCCMPFieldBridge) field).isRelationTableField();
      return result;
   }

   protected void initInsertSQL()
   {
      StringBuffer sql = new StringBuffer(250);
      sql.append(SQLUtil.INSERT_INTO)
         .append(entity.getQualifiedTableName())
         .append(" (");

      SQLUtil.getColumnNamesClause(insertFields, sql);

      sql.append(')')
         .append(SQLUtil.VALUES).append('(');
      SQLUtil.getValuesClause(insertFields, sql)
         .append(')');
      insertSQL = sql.toString();

      if(debug)
         log.debug("Insert Entity SQL: " + insertSQL);
   }

   protected void beforeInsert(EntityEnterpriseContext ctx) throws CreateException
   {
   }

   protected void performInsert(EntityEnterpriseContext ctx) throws CreateException
   {
      Connection c = null;
      PreparedStatement ps = null;
      boolean throwRuntimeExceptions = entity.getMetaData().getThrowRuntimeExceptions();

      // if metadata is true, the getconnection is done inside 
      // its own try catch block to throw a runtime exception (EJBException)
      if (throwRuntimeExceptions)
      {
          try 
          {
              c = entity.getDataSource().getConnection();
          } 
          catch (SQLException sqle) 
          {
              javax.ejb.EJBException ejbe = new javax.ejb.EJBException("Could not get a connection; " + sqle);
              //ejbe.initCause(sqle); only for JBoss 4 and +
              throw ejbe;
          } 
      }
     
      try
      {
         if(debug)
            log.debug("Executing SQL: " + insertSQL);

         // if metadata is false, the getconnection is done inside this try catch block
         if ( ! throwRuntimeExceptions)
         {
             c = entity.getDataSource().getConnection();
         }
         ps = prepareStatement(c, insertSQL, ctx);

         // set the parameters
         int index = 1;
         for(int fieldInd = 0; fieldInd < insertFields.length; ++fieldInd)
         {
            index = insertFields[fieldInd].setInstanceParameters(ps, index, ctx);
         }

         // execute statement
         int rowsAffected = executeInsert(index, ps, ctx);
         if(rowsAffected != 1)
         {
            throw new CreateException("Expected one affected row but update returned" + rowsAffected +
               " for id=" + ctx.getId());
         }
      }
      catch(SQLException e)
      {
         if(exceptionProcessor != null && exceptionProcessor.isDuplicateKey(e))
         {
            log.error("Failed to create instance.", e);
            throw new CreateException(
               "Integrity constraint violation. Possibly unique key violation or invalid foreign key value."
            );
         }
         else
         {
            log.error("Could not create entity", e);
            throw new CreateException("Could not create entity:" + e);
         }
      }
      finally
      {
         JDBCUtil.safeClose(ps);
         JDBCUtil.safeClose(c);
      }

      // Mark the inserted fields as clean.
      for(int fieldInd = 0; fieldInd < insertFields.length; ++fieldInd)
      {
         insertFields[fieldInd].setClean(ctx);
      }
   }

   protected PreparedStatement prepareStatement(Connection c, String sql, EntityEnterpriseContext ctx) throws SQLException
   {
      return c.prepareStatement(sql);
   }

   protected int executeInsert(int paramIndex, PreparedStatement ps, EntityEnterpriseContext ctx) throws SQLException
   {
      return ps.executeUpdate();
   }

   protected void afterInsert(EntityEnterpriseContext ctx) throws CreateException
   {
   }

   protected Object getPrimaryKey(EntityEnterpriseContext ctx)
   {
      return entity.extractPrimaryKeyFromInstance(ctx);
   }
}
