/**
 * 
 * Copyright 2004 Hiram Chirino
 * 
 * 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. 
 * 
 **/
package org.codehaus.activemq.store.jdbc.adapter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.message.ActiveMQXid;
import org.codehaus.activemq.service.SubscriberEntry;
import org.codehaus.activemq.service.Transaction;
import org.codehaus.activemq.service.TransactionManager;
import org.codehaus.activemq.service.impl.XATransactionCommand;
import org.codehaus.activemq.store.jdbc.JDBCAdapter;
import org.codehaus.activemq.store.jdbc.StatementProvider;
import org.codehaus.activemq.util.LongSequenceGenerator;

import javax.jms.JMSException;
import javax.transaction.xa.XAException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

/**
 * Implements all the default JDBC operations that are used
 * by the JDBCPersistenceAdapter.
 * <p/>
 * Subclassing is encouraged to override the default
 * implementation of methods to account for differences
 * in JDBC Driver implementations.
 * <p/>
 * The JDBCAdapter inserts and extracts BLOB data using the
 * getBytes()/setBytes() operations.
 * <p/>
 * The databases/JDBC drivers that use this adapter are:
 * <ul>
 * <li></li>
 * </ul>
 *
 * @version $Revision: 1.3 $
 */
public class DefaultJDBCAdapter implements JDBCAdapter {

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

    final protected StatementProvider statementProvider;
    protected LongSequenceGenerator sequenceGenerator = new LongSequenceGenerator();

    protected void setBinaryData(PreparedStatement s, int index, byte data[]) throws SQLException {
        s.setBytes(index, data);
    }

    protected byte[] getBinaryData(ResultSet rs, int index) throws SQLException {
        return rs.getBytes(index);
    }

    /**
     * @param provider
     */
    public DefaultJDBCAdapter(StatementProvider provider) {
        this.statementProvider = new CachingStatementProvider(provider);
    }

    public DefaultJDBCAdapter() {
        this(new DefaultStatementProvider());
    }

    public LongSequenceGenerator getSequenceGenerator() {
        return sequenceGenerator;
    }

    public void doCreateTables(Connection c) throws SQLException {
        Statement s = null;
        try {
            s = c.createStatement();
            String[] createStatments = statementProvider.getCreateSchemaStatments();
            for (int i = 0; i < createStatments.length; i++) {
                // This will fail usually since the tables will be
                // created allready.
                try {
                    boolean rc = s.execute(createStatments[i]);
                }
                catch (SQLException e) {
                    log.debug("Could not create JDBC tables; they could already exist. Failure was: " + createStatments[i], e);
                }
            }
            c.commit();
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void initSequenceGenerator(Connection c) {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {
            s = c.prepareStatement(statementProvider.getFindLastSequenceId());
            rs = s.executeQuery();
            if (rs.next()) {
                sequenceGenerator.setLastSequenceId(rs.getLong(1));
            }
        }
        catch (SQLException e) {
            log.warn("Failed to find last sequence number: " + e, e);
        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doAddMessage(Connection c, long seq, String messageID, String destinationName, byte[] data) throws SQLException, JMSException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getAddMessageStatment());
            s.setLong(1, seq);
            s.setString(2, destinationName);
            s.setString(3, messageID);
            setBinaryData(s, 4, data);
            if (s.executeUpdate() != 1) {
                throw new JMSException("Failed to broker message: " + messageID + " in container.  ");
            }
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

	public Long getMessageSequenceId(Connection c, String messageID) throws SQLException, JMSException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindMessageSequenceIdStatment());
            s.setString(1, messageID);
            rs = s.executeQuery();

            if (!rs.next()) {
                return null;
            }
            return new Long( rs.getLong(1) );

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
	}

    public byte[] doGetMessage(Connection c, long seq) throws SQLException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindMessageStatment());
            s.setLong(1, seq);
            rs = s.executeQuery();

            if (!rs.next()) {
                return null;
            }
            return getBinaryData(rs, 1);

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doRemoveMessage(Connection c, long seq) throws SQLException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getRemoveMessageStatment());
            s.setLong(1, seq);
            if (s.executeUpdate() != 1) {
                log.error("Could not delete sequenece number for: " + seq);
            }
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doRecover(Connection c, String destinationName, MessageListResultHandler listener) throws SQLException, JMSException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindAllMessagesStatment());
            s.setString(1, destinationName);
            rs = s.executeQuery();

            while (rs.next()) {
                long seq = rs.getLong(1);
                String msgid = rs.getString(2);
                listener.onMessage(seq, msgid);
            }

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doGetXids(Connection c, List list) throws SQLException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {
            s = c.prepareStatement(statementProvider.getFindAllXidStatment());
            rs = s.executeQuery();

            while (rs.next()) {
                String xid = rs.getString(1);
                try {
                    list.add(new ActiveMQXid(xid));
                }
                catch (JMSException e) {
                    log.error("Failed to recover prepared transaction due to invalid xid: " + xid, e);
                }
            }

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doRemoveXid(Connection c, ActiveMQXid xid) throws SQLException, XAException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getRemoveMessageStatment());
            s.setString(1, xid.toLocalTransactionId());
            if (s.executeUpdate() != 1) {
                throw new XAException("Failed to remove prepared transaction: " + xid + ".");
            }
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }


    public void doAddXid(Connection c, ActiveMQXid xid, byte[] data) throws SQLException, XAException {
        PreparedStatement s = null;
        try {

            s = c.prepareStatement(statementProvider.getAddMessageStatment());
            s.setString(1, xid.toLocalTransactionId());
            setBinaryData(s, 2, data);
            if (s.executeUpdate() != 1) {
                throw new XAException("Failed to store prepared transaction: " + xid);
            }

        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doLoadPreparedTransactions(Connection c, TransactionManager transactionManager) throws SQLException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindAllTxStatment());
            rs = s.executeQuery();

            while (rs.next()) {
                String id = rs.getString(1);
                byte data[] = this.getBinaryData(rs, 2);
                try {
                    ActiveMQXid xid = new ActiveMQXid(id);
                    Transaction transaction = XATransactionCommand.fromBytes(data);
                    transactionManager.loadTransaction(xid, transaction);
                }
                catch (Exception e) {
                    log.error("Failed to recover prepared transaction due to invalid xid: " + id, e);
                }
            }
        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    /**
     * @throws JMSException
     * @see org.codehaus.activemq.store.jdbc.JDBCAdapter#doSetLastAck(java.sql.Connection, java.lang.String, java.lang.String, long)
     */
    public void doSetLastAck(Connection c, String destinationName, String subscriptionID, long seq) throws SQLException, JMSException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getUpdateLastAckOfDurableSub());
            s.setLong(1, seq);
            s.setString(2, subscriptionID);
            s.setString(3, destinationName);

            if (s.executeUpdate() != 1) {
                throw new JMSException("Failed to acknowlege message with sequence id: " + seq + " for client: " + subscriptionID);
            }
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    /**
     * @throws JMSException
     * @see org.codehaus.activemq.store.jdbc.JDBCAdapter#doRecoverSubscription(java.sql.Connection, java.lang.String, java.lang.String, org.codehaus.activemq.store.jdbc.JDBCAdapter.MessageListResultHandler)
     */
    public void doRecoverSubscription(Connection c, String destinationName, String subscriptionID, MessageListResultHandler listener) throws SQLException, JMSException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindAllDurableSubMessagesStatment());
            s.setString(1, destinationName);
            s.setString(2, subscriptionID);
            rs = s.executeQuery();

            while (rs.next()) {
                long seq = rs.getLong(1);
                String msgid = rs.getString(2);
                listener.onMessage(seq, msgid);
            }

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    /**
     * @see org.codehaus.activemq.store.jdbc.JDBCAdapter#doSetSubscriberEntry(java.sql.Connection, java.lang.Object, org.codehaus.activemq.service.SubscriberEntry)
     */
    public void doSetSubscriberEntry(Connection c, String destinationName, String sub, SubscriberEntry subscriberEntry) throws SQLException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getUpdateDurableSubStatment());
            s.setInt(1, subscriberEntry.getSubscriberID());
            s.setString(2, subscriberEntry.getClientID());
            s.setString(3, subscriberEntry.getConsumerName());
            s.setString(4, subscriberEntry.getSelector());
            s.setString(5, sub);
            s.setString(6, destinationName);
            
            // If the sub was not there then we need to create it.
            if (s.executeUpdate() != 1) {
                s.close();
                s = c.prepareStatement(statementProvider.getCreateDurableSubStatment());
                s.setInt(1, subscriberEntry.getSubscriberID());
                s.setString(2, subscriberEntry.getClientID());
                s.setString(3, subscriberEntry.getConsumerName());
                s.setString(4, subscriberEntry.getSelector());
                s.setString(5, sub);
                s.setString(6, destinationName);
                s.setLong(7, -1);

                if (s.executeUpdate() != 1) {
                    log.error("Failed to store durable subscription for: " + sub);
                }
            }
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    /**
     * @see org.codehaus.activemq.store.jdbc.JDBCAdapter#doGetSubscriberEntry(java.sql.Connection, java.lang.Object)
     */
    public SubscriberEntry doGetSubscriberEntry(Connection c, String destinationName, String sub) throws SQLException {
        PreparedStatement s = null;
        ResultSet rs = null;
        try {

            s = c.prepareStatement(statementProvider.getFindDurableSubStatment());
            s.setString(1, sub);
            s.setString(2, destinationName);
            rs = s.executeQuery();

            if (!rs.next()) {
                return null;
            }

            SubscriberEntry answer = new SubscriberEntry();
            answer.setSubscriberID(rs.getInt(1));
            answer.setClientID(rs.getString(2));
            answer.setConsumerName(rs.getString(3));
            answer.setDestination(rs.getString(4));

            return answer;

        }
        finally {
            try {
                rs.close();
            }
            catch (Throwable e) {
            }
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

    public void doRemoveAllMessages(Connection c, String destinationName) throws SQLException, JMSException {
        PreparedStatement s = null;
        try {
            s = c.prepareStatement(statementProvider.getRemoveAllMessagesStatment());
            s.setString(1, destinationName);
            s.executeUpdate();
            s.close();
            
            s = c.prepareStatement(statementProvider.getRemoveAllSubscriptionsStatment());
            s.setString(1, destinationName);
            s.executeUpdate();
            
        }
        finally {
            try {
                s.close();
            }
            catch (Throwable e) {
            }
        }
    }

}
