/*
 * Copyright 2010 Assaf Arkin, Thomas Yip, Bruce Snyder, Werner Guttmann,
 *                Ralf Joachim, Ahmad Hassan, Dennis Butterstein
 *
 * Licensed 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.
 *
 * $Id: SQLStatementUpdate.java 8778 2010-10-24 21:34:41Z rjoachim $
 */
package org.castor.cpa.persistence.sql.engine;

import java.sql.SQLException;

import org.castor.cpa.persistence.sql.engine.info.ColumnInfo;
import org.castor.cpa.persistence.sql.engine.info.ColumnValue;
import org.castor.cpa.persistence.sql.engine.info.TableInfo;
import org.castor.cpa.persistence.sql.query.Update;
import org.castor.cpa.persistence.sql.query.condition.AndCondition;
import org.castor.cpa.persistence.sql.query.condition.Condition;
import org.castor.cpa.persistence.sql.query.expression.Column;
import org.castor.cpa.persistence.sql.query.expression.Parameter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.core.util.Messages;
import org.castor.persist.ProposedEntity;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.engine.SQLEngine;
import org.exolab.castor.jdo.engine.nature.ClassDescriptorJDONature;
import org.exolab.castor.persist.spi.Identity;

/**
 * SQLStatementStore class that makes use of Update class hierarchy to generate sql
 * query structure. It provides parameter binding support to the prepared statement
 * and then executes it.
 * 
 * @author <a href="mailto:arkin AT intalio DOT com">Assaf Arkin</a>
 * @author <a href="mailto:tyip AT leafsoft DOT com">Thomas Yip</a>
 * @author <a href="mailto:bruce DOT snyder AT gmail DOT com">Bruce Snyder</a>
 * @author <a href="mailto:werner DOT guttmann AT gmx DOT net">Werner Guttmann</a>
 * @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
 * @author <a href="mailto:ahmad DOT hassan AT gmail DOT com">Ahmad Hassan</a>
 * @author <a href="mailto:madsheepscarer AT googlemail DOT com">Dennis Butterstein</a>
 * @version $Revision: 8778 $ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
*/
public final class SQLStatementUpdate {
    //-----------------------------------------------------------------------------------    

    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
     *  Commons Logging</a> instance used for all logging. */
    private static final Log LOG = LogFactory.getLog(SQLStatementUpdate.class);
    
    /** Name space to prepend to set parameter names to distinguish them from parameters
     *  of where clause. */
    private static final String SET_PARAM_NAMESPACE = "SET:";

    //-----------------------------------------------------------------------------------    

    /** Name of engine descriptor. */
    private final String _type;

    /** Update SQL statement class hierarchy. */
    private Update _update;

    /** TableInfo object holding queried table with its relations. */
    private TableInfo _tableInfo;

    /** Indicates whether there is a field to persist at all; in the case of 
     *  EXTEND relationships where no additional attributes are defined in the 
     *  extending class, this might NOT be the case; in general, a class has to have
     *  at least one field that is to be persisted. */
    private boolean _hasFieldsToPersist;

    /** SQLStatementUpdateCheck instance to check the failure reason of the update statement.*/
    private final SQLStatementUpdateCheck _statementUpdateCheck;
    
    //-----------------------------------------------------------------------------------    

    /**
     * Constructor.
     * 
     * @param engine SQL engine for all persistence operations at entities of the type this
     *        class is responsible for. Holds all required information of the entity type.
     */
    public SQLStatementUpdate(final SQLEngine engine) {
        _type = engine.getDescriptor().getJavaClass().getName();
        _tableInfo = engine.getTableInfo();

        buildStatement(new ClassDescriptorJDONature(engine.getDescriptor()).getTableName());

        _statementUpdateCheck = new SQLStatementUpdateCheck(engine);
    }
    
    /**
     * Build SQL statement to update entities of the type this class.
     *
     * @param mapTo Table name retrieved from Class Descriptor trough JDO Nature.
     */
    private void buildStatement(final String mapTo) {
        _update = new Update(mapTo);

        // add assignments to update statement
        int count = 0;
        for (ColumnInfo col : _tableInfo.iterateAll()) {
            if (col.isStore()) {
                _update.addAssignment(new Column(col.getName()),
                        new Parameter(SET_PARAM_NAMESPACE + col.getName()));
                ++count;
            }
        }

        _hasFieldsToPersist = (count > 0);
        
        if (LOG.isTraceEnabled()) {
            LOG.trace("hasFieldsToPersist = " + _hasFieldsToPersist);
        }
    }
    
    //-----------------------------------------------------------------------------------    

    /**
     * Stores the identity to the database using JDBC Connection.
     * 
     * @param conn CastorConnection holding connection and PersistenceFactory to be used to create
     *        statement.
     * @param identity Identity of the object to update.
     * @param newentity Entity holding the new values to set with update.
     * @param oldentity Entity holding the old values to check for concurrent modifications.
     * @return Always returns <code>null</code>. 
     * @throws PersistenceException If failed to update object in database. This could happen
     *         if a database access error occurs, type of one of the values to bind is ambiguous,
     *         identity or column size mismatch or object to be updated does not exist.
     */
    public Object executeStatement(final CastorConnection conn, final Identity identity,
            final ProposedEntity newentity, final ProposedEntity oldentity)
    throws PersistenceException {
        // only execute an update statement if there are fields to persist
        if (_hasFieldsToPersist) {

            CastorStatement stmt = conn.createStatement();

            // build condition for identities
            Condition condition = new AndCondition();
            for (ColumnInfo col : _tableInfo.getPrimaryKey().getColumns()) {
                String name = col.getName();
                condition.and(new Column(name).equal(new Parameter(name)));
            }

            appendOldEntityCondition(oldentity, condition);

            try {
                stmt.prepareStatement(_update, condition);

                // bind new entity
                bindNewEntity(newentity, stmt);

                // bind identity
                bindIdentity(identity, stmt);

                // bind old entity
                bindOldEntity(oldentity, stmt);

                // executes prepared statement (SAP DB returns -1 here)
                if (stmt.executeUpdate() <= 0) {
                    // check whether the object had been modified or deleted and 
                    // raise appropriate exception
                    _statementUpdateCheck.updateFailureCheck(conn, identity);
                }
            } catch (SQLException ex) {
                LOG.fatal(Messages.format("jdo.storeFatal", _type, stmt.toString()), ex);
                throw new PersistenceException(Messages.format("persist.nested", ex), ex);
            } finally {
                // close statement
                try {
                    stmt.close();
                } catch (SQLException e) {
                    LOG.warn("Problem closing JDBC statement", e);
                }
            }
        }
        return null;
    }

    /**
     * Method that appends the fields other than identities to where condition of SQL statement.
     * 
     * @param oldentity Entity holding the old values to check for concurrent modifications.
     * @param condition Condition to add old entity Conditions.
     * @throws PersistenceException If identity size mismatches or column length mismatches
     */
    private void appendOldEntityCondition(final ProposedEntity oldentity, final Condition condition)
    throws PersistenceException {
        if (oldentity.getFields() != null) {
            for (ColumnValue value : _tableInfo.toSQL(oldentity.getFields())) {
                if (value.isStore() && value.isDirty()) {
                    String name = value.getName();
                    if (value.getValue() == null) {
                        condition.and(new Column(name).isNull());
                    } else {
                        condition.and(new Column(name).equal(new Parameter(name)));
                    }
                }
            }
        }
    }

    /**
     * Bind values of new entity to the prepared statement.
     * 
     * @param newentity Entity holding the new values to set with update.
     * @param stmt CastorStatement containing query relevant data to be extended by parameters.
     * @throws PersistenceException If identity or column size mismatches.
     * @throws SQLException If database access error occurs.
     */
    private void bindNewEntity(final ProposedEntity newentity, final CastorStatement stmt)
    throws PersistenceException, SQLException {
        //         bind fields of the row to be stored into the preparedStatement
        for (ColumnValue value : _tableInfo.toSQL(newentity.getFields())) {
            if (value.isStore()) {
                stmt.bindParameter(SET_PARAM_NAMESPACE + value.getName(), value.getValue(),
                        value.getType());
            }
        }
    }

    /**
     * Bind identity values to the prepared statement.
     * 
     * @param identity Identity of the object to update.
     * @param stmt CastorStatement containing query relevant data to be extended by parameters.
     * @throws SQLException If a database access error occurs.
     */
    private void  bindIdentity(final Identity identity, final CastorStatement stmt) 
    throws SQLException {
        // bind the identity of the row to be stored into the preparedStatement
        for (ColumnValue value : _tableInfo.toSQL(identity)) {
            stmt.bindParameter(value.getName(), value.getValue(), value.getType());

            if (LOG.isTraceEnabled()) {
                LOG.trace(Messages.format("jdo.bindingIdentity", value.getName(),
                        value.getValue()));
            }
        }
    }

    /**
     * Bind values of old entity to the prepared statement.
     * 
     * @param oldentity Entity holding the old values to check for concurrent modifications.
     * @param stmt CastorStatement containing query relevant data to be extended by parameters.
     * @throws PersistenceException If identity or column size mismatches.
     * @throws SQLException If database access error occurs.
     */
    private void bindOldEntity(final ProposedEntity oldentity, final CastorStatement stmt) 
    throws PersistenceException, SQLException {
        // bind the old fields of the row to be stored into the preparedStatement
        for (ColumnValue value : _tableInfo.toSQL(oldentity.getFields())) {
            if (value.isStore() && value.isDirty()) {
                if (value.getValue() != null) {
                    stmt.bindParameter(value.getName(), value.getValue(), value.getType());
                }
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug(Messages.format("jdo.storing", _type, stmt.toString()));
        }
    }

    //----------------------------------------------------------------------------------------------
}