/*
 * Copyright 2012 International Business Machines Corp.
 * 
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership. 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.apache.batchee.container.services.persistence;

import org.apache.batchee.container.exception.BatchContainerServiceException;
import org.apache.batchee.container.exception.PersistenceException;
import org.apache.batchee.container.impl.JobExecutionImpl;
import org.apache.batchee.container.impl.JobInstanceImpl;
import org.apache.batchee.container.impl.MetricImpl;
import org.apache.batchee.container.impl.StepContextImpl;
import org.apache.batchee.container.impl.StepExecutionImpl;
import org.apache.batchee.container.impl.controller.chunk.CheckpointData;
import org.apache.batchee.container.impl.controller.chunk.CheckpointDataKey;
import org.apache.batchee.container.impl.controller.chunk.PersistentDataWrapper;
import org.apache.batchee.container.impl.jobinstance.RuntimeFlowInSplitExecution;
import org.apache.batchee.container.impl.jobinstance.RuntimeJobExecution;
import org.apache.batchee.container.services.InternalJobExecution;
import org.apache.batchee.container.services.persistence.jdbc.Dictionary;
import org.apache.batchee.container.services.persistence.jdbc.database.Database;
import org.apache.batchee.container.services.persistence.jdbc.database.DerbyDatabase;
import org.apache.batchee.container.services.persistence.jpa.domain.CheckpointEntity;
import org.apache.batchee.container.services.persistence.jpa.domain.JobExecutionEntity;
import org.apache.batchee.container.services.persistence.jpa.domain.JobInstanceEntity;
import org.apache.batchee.container.services.persistence.jpa.domain.PropertyHelper;
import org.apache.batchee.container.services.persistence.jpa.domain.StepExecutionEntity;
import org.apache.batchee.container.status.JobStatus;
import org.apache.batchee.container.status.StepStatus;
import org.apache.batchee.container.util.TCCLObjectInputStream;
import org.apache.batchee.spi.PersistenceManagerService;

import jakarta.batch.operations.NoSuchJobExecutionException;
import jakarta.batch.runtime.BatchStatus;
import jakarta.batch.runtime.JobInstance;
import jakarta.batch.runtime.Metric;
import jakarta.batch.runtime.StepExecution;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import static org.apache.batchee.container.util.Serializations.serialize;

public class JDBCPersistenceManagerService implements PersistenceManagerService {
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    static interface Defaults {
        final String JDBC_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";
        final String JDBC_URL = "jdbc:derby:memory:batchee;create=true";
        final String JDBC_USER = "app";
        final String JDBC_PASSWORD = "app";
        final String SCHEMA = "BATCHEE";
    }

    private Dictionary dictionary;

    protected DataSource dataSource = null;
    protected String jndiName = null;

    protected String driver = "";
    protected String schema = "";
    protected String url = "";
    protected String user = "";
    protected String pwd = "";

    @Override
    public void init(final Properties batchConfig) throws BatchContainerServiceException {
        final boolean hasSchema = "true".equalsIgnoreCase(batchConfig.getProperty("persistence.database.has-schema", "true"));
        if (hasSchema) {
            schema = batchConfig.getProperty("persistence.database.schema", Defaults.SCHEMA);
        } else {
            schema = null;
        }

        if (batchConfig.containsKey("persistence.database.jndi")) {
            jndiName = batchConfig.getProperty("persistence.database.jndi", "");
            if (jndiName.isEmpty()) {
                throw new BatchContainerServiceException("JNDI name is not defined.");
            }

            try {
                final Context ctx = new InitialContext();
                dataSource = DataSource.class.cast(ctx.lookup(jndiName));
            } catch (final NamingException e) {
                throw new BatchContainerServiceException(e);
            }

        } else {
            driver = batchConfig.getProperty("persistence.database.driver", Defaults.JDBC_DRIVER);
            url = batchConfig.getProperty("persistence.database.url", Defaults.JDBC_URL);
            user = batchConfig.getProperty("persistence.database.user", Defaults.JDBC_USER);
            pwd = batchConfig.getProperty("persistence.database.password", Defaults.JDBC_PASSWORD);
        }

        try {
            initDictionary(batchConfig);

            if ("create".equalsIgnoreCase(batchConfig.getProperty("persistence.database.ddl", "create"))) {
                if (hasSchema && !isSchemaValid()) {
                    createSchema();
                }
                checkAllTables();
            }
        } catch (final SQLException e) {
            throw new BatchContainerServiceException(e);
        }
    }

    private void initDictionary(final Properties batchConfig) throws BatchContainerServiceException, SQLException {
        final String type = batchConfig.getProperty("persistence.database.db-dictionary", guessDictionary());
        if (type == null) {
            throw new IllegalArgumentException("You need to provide a db dictionary for this database");
        }

        try {
            final Database database = Database.class.cast(Thread.currentThread().getContextClassLoader().loadClass(type).newInstance());
            dictionary = new Dictionary(
                batchConfig.getProperty("persistence.database.tables.checkpoint", CheckpointEntity.TABLE_NAME),
                batchConfig.getProperty("persistence.database.tables.job-instance", JobInstanceEntity.TABLE_NAME),
                batchConfig.getProperty("persistence.database.tables.job-execution", JobExecutionEntity.TABLE_NAME),
                batchConfig.getProperty("persistence.database.tables.step-execution", StepExecutionEntity.TABLE_NAME),
                database);
        } catch (final Exception e) {
            throw new BatchContainerServiceException(e);
        }
    }

    /**
     * Checks if the default schema JBATCH or the schema defined in batch-config exists.
     *
     * @return true if the schema exists, false otherwise.
     * @throws SQLException
     */
    private boolean isSchemaValid() throws SQLException {
        final Connection conn = getConnectionToDefaultSchema();
        final DatabaseMetaData dbmd = conn.getMetaData();
        final ResultSet rs = dbmd.getSchemas();
        while (rs.next()) {
            if (schema.equalsIgnoreCase(rs.getString("TABLE_SCHEM"))) {
                cleanupConnection(conn, rs, null);
                return true;
            }
        }
        cleanupConnection(conn, rs, null);
        return false;
    }

    private String guessDictionary() throws SQLException {
        final Connection conn = getConnectionToDefaultSchema();
        final String pn;
        try {
            final DatabaseMetaData dbmd = conn.getMetaData();
            pn = dbmd.getDatabaseProductName().toLowerCase();
        } finally {
            conn.close();
        }

        if (pn.contains("derby")) {
            return DerbyDatabase.class.getName();
        }
        /* TODO
        if (pn.contains("mysql")) {
            return "mysql";
        }
        if (pn.contains("oracle")) {
            return "oracle";
        }
        if (pn.contains("hsql")) {
            return "hsql";
        }
        */
        return null;
    }

    private void createSchema() throws SQLException {
        final Connection conn = getConnectionToDefaultSchema();
        final PreparedStatement ps = conn.prepareStatement("CREATE SCHEMA " + schema);
        ps.execute();
        cleanupConnection(conn, null, ps);
    }

    /**
     * Checks if all the runtime batch table exists. If not, it creates them.
     *
     * @throws SQLException
     */
    private void checkAllTables() throws SQLException {
        createIfNotExists(dictionary.getCheckpointTable(), dictionary.getCreateCheckpointTable());
        createIfNotExists(dictionary.getJobExecutionTable(), dictionary.getCreateJobExecutionTable());
        createIfNotExists(dictionary.getJobInstanceTable(), dictionary.getCreateJobInstanceTable());
        createIfNotExists(dictionary.getStepExecutionTable(), dictionary.getCreateStepExecutionTable());
    }

    protected Connection getConnection() throws SQLException {
        final Connection connection;
        if (dataSource != null) {
            connection = dataSource.getConnection();
        } else {
            try {
                Class.forName(driver);
            } catch (ClassNotFoundException e) {
                throw new PersistenceException(e);
            }
            connection = DriverManager.getConnection(url, user, pwd);
        }
        setSchemaOnConnection(connection);

        return connection;
    }

    private void createIfNotExists(final String tableName, final String createTableStatement) throws SQLException {
        final Connection conn = getConnection();
        final DatabaseMetaData dbmd = conn.getMetaData();
        final ResultSet rs = dbmd.getTables(null, schema, tableName, null);

        PreparedStatement ps = null;
        if (!rs.next()) {
            ps = conn.prepareStatement(createTableStatement);
            ps.executeUpdate();
        }

        cleanupConnection(conn, rs, ps);
    }

    public void createCheckpointData(final CheckpointDataKey key, final CheckpointData value) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getInsertCheckpoint());

            statement.setBytes(1, value.getRestartToken());
            statement.setString(2, key.getStepName());
            statement.setString(3, key.getType().name());
            statement.setLong(4, key.getJobInstanceId());
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public CheckpointData getCheckpointData(final CheckpointDataKey key) {
        return queryCheckpointData(key);
    }

    @Override
    public void setCheckpointData(final CheckpointDataKey key, final CheckpointData value) {
        if (queryCheckpointData(key) != null) {
            updateCheckpointData(key, value);
        } else {
            createCheckpointData(key, value);
        }
    }

    /**
     * @return the database connection. The schema is set to whatever default its used by the underlying database.
     * @throws SQLException
     */
    protected Connection getConnectionToDefaultSchema() throws SQLException {
        final Connection connection;
        if (dataSource != null) {
            try {
                connection = dataSource.getConnection();
            } catch (final SQLException e) {
                throw new PersistenceException(e);
            }
        } else {
            try {
                Class.forName(driver);
            } catch (final ClassNotFoundException e) {
                final StringWriter sw = new StringWriter();
                final PrintWriter pw = new PrintWriter(sw);
                e.printStackTrace(pw);
                throw new PersistenceException(e);
            }
            try {
                connection = DriverManager.getConnection(url, user, pwd);
            } catch (final SQLException e) {
                throw new PersistenceException(e);
            }
        }
        return connection;
    }

    private void setSchemaOnConnection(final Connection connection) throws SQLException {
        if (schema == null) {
            return;
        }

        final PreparedStatement ps = connection.prepareStatement("set schema ?");
        ps.setString(1, schema);
        ps.executeUpdate();
        ps.close();
    }

    /**
     * select data from DB table
     *
     * @param key - the IPersistenceDataKey object
     * @return List of serializable objects store in the DB table
     * <p/>
     * Ex. select id, obj from tablename where id = ?
     */
    private CheckpointData queryCheckpointData(final CheckpointDataKey key) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getSelectCheckpoint());
            statement.setLong(1, key.getJobInstanceId());
            statement.setString(2, key.getType().name());
            statement.setString(3, key.getStepName());
            rs = statement.executeQuery();
            if (rs.next()) {
                final CheckpointData data = new CheckpointData(key.getJobInstanceId(), key.getStepName(), key.getType());
                data.setRestartToken(rs.getBytes(dictionary.checkpointColumn(1)));
                return data;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    /**
     * update data in DB table
     *
     * @param value - serializable object to store
     * @param key   - the IPersistenceDataKey object
     *              <p/>
     *              Ex. update tablename set obj = ? where id = ?
     */
    private void updateCheckpointData(final CheckpointDataKey key, final CheckpointData value) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateCheckpoint());
            statement.setBytes(1, value.getRestartToken());
            statement.setLong(2, key.getJobInstanceId());
            statement.setString(3, key.getType().name());
            statement.setString(4, key.getStepName());
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }


    /**
     * closes connection, result set and statement
     *
     * @param conn      - connection object to close
     * @param rs        - result set object to close
     * @param statement - statement object to close
     */
    private void cleanupConnection(final Connection conn, final ResultSet rs, final PreparedStatement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (final SQLException e) {
                throw new PersistenceException(e);
            }
        }

        if (rs != null) {
            try {
                rs.close();
            } catch (final SQLException e) {
                throw new PersistenceException(e);
            }
        }

        if (conn != null) {
            Exception thrown = null;
            try {
                conn.close();
            } catch (final SQLException e) {
                throw new PersistenceException(e);
            } finally {
                try {
                    conn.close();
                } catch (final SQLException e) {
                    thrown = e;
                }
            }
            if (thrown != null) {
                throw new PersistenceException(thrown);
            }
        }
    }

    @Override
    public int jobOperatorGetJobInstanceCount(final String jobName) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getCountJobInstanceByName());
            statement.setString(1, jobName);
            rs = statement.executeQuery();
            rs.next();
            return rs.getInt("jobinstancecount"); // not a column name so ok
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public List<Long> jobOperatorGetJobInstanceIds(final String jobName, final int start, final int count) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        final List<Long> data = new ArrayList<Long>();

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobInstanceIdsByName());
            statement.setObject(1, jobName);
            rs = statement.executeQuery();
            while (rs.next()) {
                data.add(rs.getLong(dictionary.jobInstanceColumns(0)));
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }

        if (data.size() > 0) {
            try {
                return data.subList(start, start + count);
            } catch (final IndexOutOfBoundsException oobEx) {
                return data.subList(start, data.size());
            }
        }
        return data;
    }

    @Override
    public Set<String> getJobNames() {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        Set<String> jobNames = new HashSet<String>();
        try {
            conn = getConnection();

            statement = conn.prepareStatement(dictionary.getFindJobNames());
            rs = statement.executeQuery();
            while (rs.next()) {
                jobNames.add(rs.getString(1));
            }

        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
        return jobNames;
    }

    @Override
    public Timestamp jobOperatorQueryJobExecutionTimestamp(final long key, final TimestampType timestampType) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        Timestamp createTimestamp = null;
        Timestamp endTimestamp = null;
        Timestamp updateTimestamp = null;
        Timestamp startTimestamp = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionTimestamps());
            statement.setLong(1, key);
            rs = statement.executeQuery();
            while (rs.next()) {
                createTimestamp = rs.getTimestamp(1);
                endTimestamp = rs.getTimestamp(2);
                updateTimestamp = rs.getTimestamp(3);
                startTimestamp = rs.getTimestamp(4);
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }

        if (timestampType.equals(TimestampType.CREATE)) {
            return createTimestamp;
        } else if (timestampType.equals(TimestampType.END)) {
            return endTimestamp;
        } else if (timestampType.equals(TimestampType.LAST_UPDATED)) {
            return updateTimestamp;
        } else if (timestampType.equals(TimestampType.STARTED)) {
            return startTimestamp;
        }
        throw new IllegalArgumentException("Unexpected enum value.");
    }

    @Override
    public String jobOperatorQueryJobExecutionBatchStatus(final long key) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionBatchStatus());
            statement.setLong(1, key);
            rs = statement.executeQuery();
            if (rs.next()) {
                return rs.getString(1);
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }


    @Override
    public String jobOperatorQueryJobExecutionExitStatus(final long key) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionExitStatus());
            statement.setLong(1, key);
            rs = statement.executeQuery();
            if (rs.next()) {
                return rs.getString(1);
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public Properties getParameters(final long executionId) throws NoSuchJobExecutionException {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionJobProperties());
            statement.setLong(1, executionId);
            rs = statement.executeQuery();

            if (rs.next()) {
                final byte[] buf = rs.getBytes(dictionary.jobExecutionColumns(5));
                return PropertyHelper.stringToProperties(buf != null ? new String(buf) : null);
            }
            throw new NoSuchJobExecutionException("Did not find table entry for executionID =" + executionId);
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public List<StepExecution> getStepExecutionsForJobExecution(final long execid) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        long jobexecid;
        long stepexecid;
        String stepname;
        String batchstatus;
        String exitstatus;
        long readCount;
        long writeCount;
        long commitCount;
        long rollbackCount;
        long readSkipCount;
        long processSkipCount;
        long filterCount;
        long writeSkipCount;
        Timestamp startTS;
        Timestamp endTS;
        StepExecutionImpl stepEx;
        ObjectInputStream objectIn;

        final List<StepExecution> data = new ArrayList<StepExecution>();

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFinStepExecutionFromJobExecution());
            statement.setLong(1, execid);
            rs = statement.executeQuery();

            while (rs.next()) {
                jobexecid = rs.getLong(dictionary.stepExecutionColumns(18));
                stepexecid = rs.getLong(dictionary.stepExecutionColumns(0));
                stepname = rs.getString(dictionary.stepExecutionColumns(15));
                batchstatus = rs.getString(dictionary.stepExecutionColumns(1));
                exitstatus = rs.getString(dictionary.stepExecutionColumns(4));
                readCount = rs.getLong(dictionary.stepExecutionColumns(10));
                writeCount = rs.getLong(dictionary.stepExecutionColumns(16));
                commitCount = rs.getLong(dictionary.stepExecutionColumns(2));
                rollbackCount = rs.getLong(dictionary.stepExecutionColumns(12));
                readSkipCount = rs.getLong(dictionary.stepExecutionColumns(11));
                processSkipCount = rs.getLong(dictionary.stepExecutionColumns(9));
                filterCount = rs.getLong(dictionary.stepExecutionColumns(5));
                writeSkipCount = rs.getLong(dictionary.stepExecutionColumns(17));
                startTS = rs.getTimestamp(dictionary.stepExecutionColumns(14));
                endTS = rs.getTimestamp(dictionary.stepExecutionColumns(3));

                // get the object based data
                Serializable persistentData = null;
                final byte[] pDataBytes = rs.getBytes(dictionary.stepExecutionColumns(8));
                if (pDataBytes != null) {
                    objectIn = new TCCLObjectInputStream(new ByteArrayInputStream(pDataBytes));
                    persistentData = (Serializable) objectIn.readObject();
                }

                stepEx = new StepExecutionImpl(jobexecid, stepexecid);

                stepEx.setBatchStatus(BatchStatus.valueOf(batchstatus));
                stepEx.setExitStatus(exitstatus);
                stepEx.setStepName(stepname);
                stepEx.setReadCount(readCount);
                stepEx.setWriteCount(writeCount);
                stepEx.setCommitCount(commitCount);
                stepEx.setRollbackCount(rollbackCount);
                stepEx.setReadSkipCount(readSkipCount);
                stepEx.setProcessSkipCount(processSkipCount);
                stepEx.setFilterCount(filterCount);
                stepEx.setWriteSkipCount(writeSkipCount);
                stepEx.setStartTime(startTS);
                stepEx.setEndTime(endTS);
                stepEx.setPersistentUserData(persistentData);

                data.add(stepEx);
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } catch (final IOException e) {
            throw new PersistenceException(e);
        } catch (final ClassNotFoundException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }

        return data;
    }


    @Override
    public StepExecution getStepExecutionByStepExecutionId(final long stepExecId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindStepExecutionFromId());
            statement.setLong(1, stepExecId);
            rs = statement.executeQuery();
            if (rs.next()) {
                final long jobexecid = rs.getLong(dictionary.stepExecutionColumns(18));
                final long stepExecutionId = rs.getLong(dictionary.stepExecutionColumns(0));
                final String stepName = rs.getString(dictionary.stepExecutionColumns(15));
                final String batchstatus = rs.getString(dictionary.stepExecutionColumns(1));
                final String exitstatus = rs.getString(dictionary.stepExecutionColumns(4));
                final long readCount = rs.getLong(dictionary.stepExecutionColumns(10));
                final long writeCount = rs.getLong(dictionary.stepExecutionColumns(16));
                final long commitCount = rs.getLong(dictionary.stepExecutionColumns(2));
                final long rollbackCount = rs.getLong(dictionary.stepExecutionColumns(12));
                final long readSkipCount = rs.getLong(dictionary.stepExecutionColumns(11));
                final long processSkipCount = rs.getLong(dictionary.stepExecutionColumns(9));
                final long filterCount = rs.getLong(dictionary.stepExecutionColumns(5));
                final long writeSkipCount = rs.getLong(dictionary.stepExecutionColumns(17));
                final Timestamp startTS = rs.getTimestamp(dictionary.stepExecutionColumns(14));
                final Timestamp endTS = rs.getTimestamp(dictionary.stepExecutionColumns(3));

                // get the object based data
                Serializable persistentData = null;
                final byte[] pDataBytes = rs.getBytes(dictionary.stepExecutionColumns(8));
                if (pDataBytes != null) {
                    persistentData = Serializable.class.cast(new TCCLObjectInputStream(new ByteArrayInputStream(pDataBytes)).readObject());
                }

                final StepExecutionImpl stepEx = new StepExecutionImpl(jobexecid, stepExecutionId);

                stepEx.setBatchStatus(BatchStatus.valueOf(batchstatus));
                stepEx.setExitStatus(exitstatus);
                stepEx.setStepName(stepName);
                stepEx.setReadCount(readCount);
                stepEx.setWriteCount(writeCount);
                stepEx.setCommitCount(commitCount);
                stepEx.setRollbackCount(rollbackCount);
                stepEx.setReadSkipCount(readSkipCount);
                stepEx.setProcessSkipCount(processSkipCount);
                stepEx.setFilterCount(filterCount);
                stepEx.setWriteSkipCount(writeSkipCount);
                stepEx.setStartTime(startTS);
                stepEx.setEndTime(endTS);
                stepEx.setPersistentUserData(persistentData);
                return stepEx;
            }
            return null;
        } catch (SQLException e) {
            throw new PersistenceException(e);
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (ClassNotFoundException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public void updateBatchStatusOnly(final long key, final BatchStatus batchStatus, final Timestamp updatets) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateJobExecution());
            statement.setString(1, batchStatus.name());
            statement.setTimestamp(2, updatets);
            statement.setLong(3, key);
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            e.printStackTrace();
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public void updateWithFinalExecutionStatusesAndTimestamps(final long key, final BatchStatus batchStatus,
                                                              final String exitStatus, final Timestamp updatets) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getSetJobExecutionFinalData());

            statement.setString(1, batchStatus.name());
            statement.setString(2, exitStatus);
            statement.setTimestamp(3, updatets);
            statement.setTimestamp(4, updatets);
            statement.setLong(5, key);

            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            e.printStackTrace();
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    public void markJobStarted(final long key, final Timestamp startTS) {
        Connection conn = null;
        PreparedStatement statement = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateStartedJobExecution());

            statement.setString(1, BatchStatus.STARTED.name());
            statement.setTimestamp(2, startTS);
            statement.setTimestamp(3, startTS);
            statement.setLong(4, key);

            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }


    @Override
    public InternalJobExecution jobOperatorGetJobExecution(long jobExecutionId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionById());
            statement.setLong(1, jobExecutionId);
            rs = statement.executeQuery();
            if (rs.next()) {
                final Timestamp createtime = rs.getTimestamp(dictionary.jobExecutionColumns(2));
                final Timestamp starttime = rs.getTimestamp(dictionary.jobExecutionColumns(6));
                final Timestamp endtime = rs.getTimestamp(dictionary.jobExecutionColumns(3));
                final Timestamp updatetime = rs.getTimestamp(dictionary.jobExecutionColumns(7));
                final long instanceId = rs.getLong(dictionary.jobExecutionColumns(8));

                // get the object based data
                final String batchStatus = rs.getString(dictionary.jobExecutionColumns(1));
                final String exitStatus = rs.getString(dictionary.jobExecutionColumns(4));

                // get the object based data
                final byte[] buf = rs.getBytes(dictionary.jobExecutionColumns(5));
                final Properties params = Properties.class.cast(PropertyHelper.stringToProperties(buf != null ? new String(buf) : null));

                final String jobName = rs.getString(dictionary.jobInstanceColumns(3));

                final JobExecutionImpl jobEx = new JobExecutionImpl(jobExecutionId, instanceId, this);
                jobEx.setCreateTime(createtime);
                jobEx.setStartTime(starttime);
                jobEx.setEndTime(endtime);
                jobEx.setJobParameters(params);
                jobEx.setLastUpdateTime(updatetime);
                jobEx.setBatchStatus(batchStatus);
                jobEx.setExitStatus(exitStatus);
                jobEx.setJobName(jobName);
                return jobEx;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public List<InternalJobExecution> jobOperatorGetJobExecutions(final long jobInstanceId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        final List<InternalJobExecution> data = new ArrayList<InternalJobExecution>();
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobExecutionByInstance());
            statement.setLong(1, jobInstanceId);
            rs = statement.executeQuery();
            while (rs.next()) {
                final long jobExecutionId = rs.getLong(dictionary.jobExecutionColumns(0));
                final Timestamp createtime = rs.getTimestamp(dictionary.jobExecutionColumns(2));
                final Timestamp starttime = rs.getTimestamp(dictionary.jobExecutionColumns(6));
                final Timestamp endtime = rs.getTimestamp(dictionary.jobExecutionColumns(3));
                final Timestamp updatetime = rs.getTimestamp(dictionary.jobExecutionColumns(7));
                final String batchStatus = rs.getString(dictionary.jobExecutionColumns(1));
                final String exitStatus = rs.getString(dictionary.jobExecutionColumns(4));
                final String jobName = rs.getString(dictionary.jobInstanceColumns(3));
                final byte[] buf = rs.getBytes(dictionary.jobExecutionColumns(5));
                final Properties params = buf != null ? PropertyHelper.stringToProperties(new String(buf)) : null;

                final JobExecutionImpl jobEx = new JobExecutionImpl(jobExecutionId, jobInstanceId, this);
                jobEx.setCreateTime(createtime);
                jobEx.setStartTime(starttime);
                jobEx.setEndTime(endtime);
                jobEx.setLastUpdateTime(updatetime);
                jobEx.setBatchStatus(batchStatus);
                jobEx.setExitStatus(exitStatus);
                jobEx.setJobName(jobName);
                jobEx.setJobParameters(params);

                data.add(jobEx);
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
        return data;
    }

    @Override
    public Set<Long> jobOperatorGetRunningExecutions(String jobName) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        final Set<Long> executionIds = new HashSet<Long>();
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindRunningJobExecutions());
            statement.setString(1, BatchStatus.STARTED.name());
            statement.setString(2, BatchStatus.STARTING.name());
            statement.setString(3, BatchStatus.STOPPING.name());
            statement.setString(4, jobName);
            rs = statement.executeQuery();
            while (rs.next()) {
                executionIds.add(rs.getLong(dictionary.jobExecutionColumns(0)));
            }

        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
        return executionIds;
    }

    @Override
    public JobStatus getJobStatusFromExecution(final long executionId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobStatus());
            statement.setLong(1, executionId);
            rs = statement.executeQuery();
            if (rs.next()) {
                final long jobInstanceId = rs.getLong(dictionary.jobExecutionColumns(8));
                final String batchStatus = rs.getString(dictionary.jobExecutionColumns(1));
                final byte[] jobXmls = rs.getBytes(dictionary.jobInstanceColumns(4));
                final JobInstanceImpl jobInstance;
                if (jobXmls != null) {
                    jobInstance = new JobInstanceImpl(jobInstanceId, new String(jobXmls, UTF_8));
                } else {
                    jobInstance = new JobInstanceImpl(jobInstanceId);
                }
                jobInstance.setJobName(rs.getString(dictionary.jobInstanceColumns(3)));

                final JobStatus status = new JobStatus(jobInstanceId);
                status.setExitStatus(rs.getString(dictionary.jobInstanceColumns(2)));
                status.setLatestExecutionId(rs.getLong(dictionary.jobInstanceColumns(5)));
                status.setRestartOn(rs.getString(dictionary.jobInstanceColumns(6)));
                status.setCurrentStepId(rs.getString(dictionary.jobInstanceColumns(7)));
                status.setJobInstance(jobInstance);
                if (batchStatus != null) {
                    status.setBatchStatus(BatchStatus.valueOf(batchStatus));
                }

                return status;
            }
            return null;
        } catch (final Exception e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    public long getJobInstanceIdByExecutionId(final long executionId) throws NoSuchJobExecutionException {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobInstanceFromJobExecution());
            statement.setObject(1, executionId);
            rs = statement.executeQuery();
            if (!rs.next()) {
                throw new NoSuchJobExecutionException("Did not find job instance associated with executionID =" + executionId);
            }
            return rs.getLong(dictionary.jobExecutionColumns(8));
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public JobInstance createSubJobInstance(final String name) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        JobInstanceImpl jobInstance = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getCreateJobInstance(), Statement.RETURN_GENERATED_KEYS);
            statement.setString(1, name);
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
            rs = statement.getGeneratedKeys();
            if (rs.next()) {
                final long jobInstanceID = rs.getLong(1);
                jobInstance = new JobInstanceImpl(jobInstanceID);
                jobInstance.setJobName(name);
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
        return jobInstance;
    }

    @Override
    public JobInstance createJobInstance(final String name, final String jobXml) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getCreateJobInstanceWithJobXml(), Statement.RETURN_GENERATED_KEYS);
            statement.setString(1, name);
            statement.setBytes(2, jobXml.getBytes(UTF_8));
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
            rs = statement.getGeneratedKeys();
            if (rs.next()) {
                long jobInstanceID = rs.getLong(1);
                final JobInstanceImpl jobInstance = new JobInstanceImpl(jobInstanceID, jobXml);
                jobInstance.setJobName(name);
                return jobInstance;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public RuntimeJobExecution createJobExecution(final JobInstance jobInstance, final Properties jobParameters, final BatchStatus batchStatus) {
        final Timestamp now = new Timestamp(System.currentTimeMillis());
        final long newExecutionId = createRuntimeJobExecutionEntry(jobInstance, jobParameters, batchStatus, now);
        final RuntimeJobExecution jobExecution = new RuntimeJobExecution(jobInstance, newExecutionId, this);
        jobExecution.setBatchStatus(batchStatus.name());
        jobExecution.setCreateTime(now);
        jobExecution.setLastUpdateTime(now);
        return jobExecution;
    }

    private long createRuntimeJobExecutionEntry(final JobInstance jobInstance, final Properties jobParameters, final BatchStatus batchStatus,
                                                final Timestamp timestamp) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getCreateJobExecution(), Statement.RETURN_GENERATED_KEYS);
            statement.setLong(1, jobInstance.getInstanceId());
            statement.setTimestamp(2, timestamp);
            statement.setTimestamp(3, timestamp);
            statement.setString(4, batchStatus.name());
            String propVal = PropertyHelper.propertiesToString(jobParameters);
            statement.setObject(5, propVal != null ? propVal.getBytes() : null);
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
            rs = statement.getGeneratedKeys();
            if (rs.next()) {
                return rs.getLong(1);
            }
            return -1;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public RuntimeFlowInSplitExecution createFlowInSplitExecution(final JobInstance jobInstance, final BatchStatus batchStatus) {
        final Timestamp now = new Timestamp(System.currentTimeMillis());
        final long newExecutionId = createRuntimeJobExecutionEntry(jobInstance, null, batchStatus, now);
        final RuntimeFlowInSplitExecution flowExecution = new RuntimeFlowInSplitExecution(jobInstance, newExecutionId, this);
        flowExecution.setBatchStatus(batchStatus.name());
        flowExecution.setCreateTime(now);
        flowExecution.setLastUpdateTime(now);
        return flowExecution;
    }

    @Override
    public StepExecutionImpl createStepExecution(final long rootJobExecId, final StepContextImpl stepContext) {
        final String batchStatus = stepContext.getBatchStatus() == null ? BatchStatus.STARTING.name() : stepContext.getBatchStatus().name();
        final String exitStatus = stepContext.getExitStatus();
        final String stepName = stepContext.getStepName();

        long readCount = 0;
        long writeCount = 0;
        long commitCount = 0;
        long rollbackCount = 0;
        long readSkipCount = 0;
        long processSkipCount = 0;
        long filterCount = 0;
        long writeSkipCount = 0;
        Timestamp startTime = stepContext.getStartTimeTS();
        Timestamp endTime = stepContext.getEndTimeTS();

        final Metric[] metrics = stepContext.getMetrics();
        for (final Metric metric : metrics) {
            if (metric.getType().equals(MetricImpl.MetricType.READ_COUNT)) {
                readCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.WRITE_COUNT)) {
                writeCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.PROCESS_SKIP_COUNT)) {
                processSkipCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.COMMIT_COUNT)) {
                commitCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.ROLLBACK_COUNT)) {
                rollbackCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.READ_SKIP_COUNT)) {
                readSkipCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.FILTER_COUNT)) {
                filterCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.WRITE_SKIP_COUNT)) {
                writeSkipCount = metric.getValue();
            }
        }
        final Serializable persistentData = stepContext.getPersistentUserData();

        return createStepExecution(rootJobExecId, batchStatus, exitStatus, stepName, readCount,
            writeCount, commitCount, rollbackCount, readSkipCount, processSkipCount, filterCount, writeSkipCount, startTime,
            endTime, persistentData);
    }


//CHECKSTYLE:OFF
    private StepExecutionImpl createStepExecution(final long rootJobExecId, final String batchStatus, final String exitStatus, final String stepName,
                                                  final long readCount, final long writeCount, final long commitCount, final long rollbackCount, final long readSkipCount,
                                                  final long processSkipCount, final long filterCount, final long writeSkipCount, final Timestamp startTime,
                                                  final Timestamp endTime, final Serializable persistentData) {
//CHECKSTYLE:ON
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getCreateStepExecution(), Statement.RETURN_GENERATED_KEYS);
            statement.setLong(1, rootJobExecId);
            statement.setString(2, batchStatus);
            statement.setString(3, exitStatus);
            statement.setString(4, stepName);
            statement.setLong(5, readCount);
            statement.setLong(6, writeCount);
            statement.setLong(7, commitCount);
            statement.setLong(8, rollbackCount);
            statement.setLong(9, readSkipCount);
            statement.setLong(10, processSkipCount);
            statement.setLong(11, filterCount);
            statement.setLong(12, writeSkipCount);
            statement.setTimestamp(13, startTime);
            statement.setTimestamp(14, endTime);
            statement.setObject(15, serialize(persistentData));

            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }

            rs = statement.getGeneratedKeys();
            if (rs.next()) {
                final long stepExecutionId = rs.getLong(1);
                final StepExecutionImpl stepExecution = new StepExecutionImpl(rootJobExecId, stepExecutionId);
                stepExecution.setStepName(stepName);
                return stepExecution;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } catch (final IOException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public void updateStepExecution(final long rootJobExecId, final StepContextImpl stepContext) {
        final long stepExecutionId = stepContext.getStepInternalExecID();
        final String batchStatus = stepContext.getBatchStatus() == null ? BatchStatus.STARTING.name() : stepContext.getBatchStatus().name();
        final String exitStatus = stepContext.getExitStatus();
        final String stepName = stepContext.getStepName();
        final Timestamp startTime = stepContext.getStartTimeTS();
        final Timestamp endTime = stepContext.getEndTimeTS();

        long readCount = 0;
        long writeCount = 0;
        long commitCount = 0;
        long rollbackCount = 0;
        long readSkipCount = 0;
        long processSkipCount = 0;
        long filterCount = 0;
        long writeSkipCount = 0;

        final Metric[] metrics = stepContext.getMetrics();
        for (final Metric metric : metrics) {
            if (metric.getType().equals(MetricImpl.MetricType.READ_COUNT)) {
                readCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.WRITE_COUNT)) {
                writeCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.PROCESS_SKIP_COUNT)) {
                processSkipCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.COMMIT_COUNT)) {
                commitCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.ROLLBACK_COUNT)) {
                rollbackCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.READ_SKIP_COUNT)) {
                readSkipCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.FILTER_COUNT)) {
                filterCount = metric.getValue();
            } else if (metric.getType().equals(MetricImpl.MetricType.WRITE_SKIP_COUNT)) {
                writeSkipCount = metric.getValue();
            }
        }
        final Serializable persistentData = stepContext.getPersistentUserData();

        updateStepExecution(stepExecutionId, rootJobExecId, batchStatus, exitStatus, stepName, readCount,
            writeCount, commitCount, rollbackCount, readSkipCount, processSkipCount, filterCount,
            writeSkipCount, startTime, endTime, persistentData);

    }


//CHECKSTYLE:OFF
    private void updateStepExecution(final long stepExecutionId, final long jobExecId, final String batchStatus, final String exitStatus, final String stepName,
                                     final long readCount, final long writeCount, final long commitCount, final long rollbackCount, final long readSkipCount,
                                     final long processSkipCount, final long filterCount, final long writeSkipCount, final Timestamp startTime,
                                     final Timestamp endTime, final Serializable persistentData) {
//CHECKSTYLE:ON
        Connection conn = null;
        PreparedStatement statement = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateStepExecution());
            statement.setLong(1, jobExecId);
            statement.setString(2, batchStatus);
            statement.setString(3, exitStatus);
            statement.setString(4, stepName);
            statement.setLong(5, readCount);
            statement.setLong(6, writeCount);
            statement.setLong(7, commitCount);
            statement.setLong(8, rollbackCount);
            statement.setLong(9, readSkipCount);
            statement.setLong(10, processSkipCount);
            statement.setLong(11, filterCount);
            statement.setLong(12, writeSkipCount);
            statement.setTimestamp(13, startTime);
            statement.setTimestamp(14, endTime);
            statement.setObject(15, serialize(persistentData));
            statement.setLong(16, stepExecutionId);
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } catch (final IOException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public JobStatus createJobStatus(final long jobInstanceId) {
        return new JobStatus(jobInstanceId); // instance already created
    }

    @Override
    public JobStatus getJobStatus(final long instanceId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindJobInstance());
            statement.setLong(1, instanceId);
            rs = statement.executeQuery();
            if (rs.next()) {
                final JobStatus status = new JobStatus(instanceId);
                status.setCurrentStepId(dictionary.jobExecutionColumns(7));
                status.setExitStatus(dictionary.jobExecutionColumns(2));

                final byte[] jobXmls = rs.getBytes(dictionary.jobInstanceColumns(4));
                final JobInstanceImpl instance;
                if (jobXmls != null) {
                    instance = new JobInstanceImpl(instanceId, new String(jobXmls, UTF_8));
                } else {
                    instance = new JobInstanceImpl(instanceId);
                }
                instance.setJobName(rs.getString(dictionary.jobInstanceColumns(3)));

                status.setJobInstance(instance);

                status.setLatestExecutionId(rs.getLong(dictionary.jobInstanceColumns(5)));
                status.setRestartOn(rs.getString(dictionary.jobInstanceColumns(6)));

                final String batchStatus = rs.getString(dictionary.stepExecutionColumns(1));
                if (batchStatus != null) {
                    status.setBatchStatus(BatchStatus.valueOf(batchStatus));
                }

                return status;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    /* (non-Javadoc)
     * @see org.apache.batchee.spi.PersistenceManagerService#updateJobStatus(long, org.apache.batchee.container.status.JobStatus)
     */
    @Override
    public void updateJobStatus(final long instanceId, final JobStatus jobStatus) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateJobInstanceStatus());
            if (jobStatus.getBatchStatus() != null) {
                statement.setString(1, jobStatus.getBatchStatus().name());
            } else {
                statement.setString(1, null);
            }
            statement.setString(2, jobStatus.getExitStatus());
            statement.setLong(3, jobStatus.getLatestExecutionId());
            statement.setString(4, jobStatus.getRestartOn());
            statement.setString(5, jobStatus.getCurrentStepId());
            if (jobStatus.getJobInstance() != null) {
                statement.setString(6, jobStatus.getJobInstance().getJobName());
            } else {
                statement.setString(6, null);
            }
            statement.setLong(7, instanceId);
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public StepStatus createStepStatus(final long stepExecId) {
        return new StepStatus(stepExecId); // instance already created
    }

    @Override
    public StepStatus getStepStatus(final long instanceId, final String stepName) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindStepExecutionByJobInstanceAndStepName());
            statement.setLong(1, instanceId);
            statement.setString(2, stepName);
            rs = statement.executeQuery();
            if (rs.next()) {
                final int startCount = rs.getInt(dictionary.stepExecutionColumns(13));
                final long id = rs.getLong(dictionary.stepExecutionColumns(0));
                final StepStatus stepStatus = new StepStatus(id, startCount);
                final int numPartitions = rs.getInt(dictionary.stepExecutionColumns(7));
                final byte[] persistentDatas = rs.getBytes(dictionary.stepExecutionColumns(8));
                if (numPartitions >= 0) {
                    stepStatus.setNumPartitions(numPartitions);
                }
                if (persistentDatas != null) {
                    stepStatus.setPersistentUserData(new PersistentDataWrapper(persistentDatas));
                }
                stepStatus.setBatchStatus(BatchStatus.valueOf(rs.getString(dictionary.stepExecutionColumns(1))));
                stepStatus.setExitStatus(rs.getString(dictionary.stepExecutionColumns(4)));
                stepStatus.setLastRunStepExecutionId(rs.getLong(dictionary.stepExecutionColumns(6)));
                stepStatus.setStepExecutionId(id);
                return stepStatus;
            }
            return null;
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
    }

    @Override
    public void updateStepStatus(final long stepExecutionId, final StepStatus stepStatus) {
        Connection conn = null;
        PreparedStatement statement = null;
        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getUpdateStepExecutionStatus());
            statement.setBytes(1, stepStatus.getRawPersistentUserData());
            if (stepStatus.getBatchStatus() != null) {
                statement.setString(2, stepStatus.getBatchStatus().name());
            } else {
                statement.setString(2, null);
            }
            statement.setString(3, stepStatus.getExitStatus());
            statement.setLong(4, stepStatus.getLastRunStepExecutionId());
            if (stepStatus.getNumPartitions() != null) {
                statement.setInt(5, stepStatus.getNumPartitions());
            } else {
                statement.setInt(5, -1);
            }
            statement.setInt(6, stepStatus.getStartCount());
            statement.setLong(7, stepStatus.getStepExecutionId());
            statement.executeUpdate();
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, statement);
        }
    }

    @Override
    public long getMostRecentExecutionId(final long jobInstanceId) {
        Connection conn = null;
        PreparedStatement statement = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            statement = conn.prepareStatement(dictionary.getFindMostRecentJobExecution());
            statement.setLong(1, jobInstanceId);
            rs = statement.executeQuery();
            if (rs.next()) {
                return rs.getLong(1);
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, rs, statement);
        }
        return -1;
    }

    @Override
    public void cleanUp(final long instanceId) {
        Connection conn = null;
        try {
            conn = getConnection();
            deleteFromInstanceId(instanceId, conn, dictionary.getDeleteStepExecution());
            deleteFromInstanceId(instanceId, conn, dictionary.getDeleteCheckpoint());
            deleteFromInstanceId(instanceId, conn, dictionary.getDeleteJobExecution());
            deleteFromInstanceId(instanceId, conn, dictionary.getDeleteJobInstance());
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, null);
        }
    }

    @Override
    public void cleanUp(final Date until) {
        Connection conn = null;
        try {
            conn = getConnection();
            deleteUntil(until, conn, dictionary.getDeleteStepExecutionUntil());
            deleteUntil(until, conn, dictionary.getDeleteCheckpointUntil());
            deleteUntil(until, conn, dictionary.getDeleteJobInstanceUntil());
            deleteUntil(until, conn, dictionary.getDeleteJobExecutionUntil());
            if (!conn.getAutoCommit()) {
                conn.commit();
            }
        } catch (final SQLException e) {
            throw new PersistenceException(e);
        } finally {
            cleanupConnection(conn, null, null);
        }
    }

    private static void deleteUntil(final Date until, final Connection conn, final String delete) throws SQLException {
        PreparedStatement statement = null;
        try {
            statement = conn.prepareStatement(delete);
            statement.setTimestamp(1, new Timestamp(until.getTime()));
            statement.executeUpdate();
        } finally {
            if (statement != null) {
                statement.close();
            }
        }
    }

    private static void deleteFromInstanceId(final long instanceId, final Connection conn, final String delete) throws SQLException {
        PreparedStatement statement = null;
        try {
            statement = conn.prepareStatement(delete);
            statement.setLong(1, instanceId);
            statement.executeUpdate();
        } finally {
            if (statement != null) {
                statement.close();
            }
        }
    }

    @Override
    public String toString() {
        try {
            return "JDBCPersistenceManager{" +
                "dictionary=" + dictionary +
                ", dataSource=" + dataSource +
                ", jndiName='" + jndiName + '\'' +
                ", driver='" + driver + '\'' +
                ", schema='" + schema + '\'' +
                ", url='" + url + '\'' +
                ", user='" + user + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
        } catch (final Throwable th) {
            return "JDBCPersistenceManager{dictionary=" + dictionary + '}';
        }
    }
}
