/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.helper;

import java.sql.*;
import java.util.*;
import org.eclipse.persistence.internal.expressions.SQLSelectStatement;
import org.eclipse.persistence.internal.expressions.ForUpdateClause;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.platform.database.OraclePlatform;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.databaseaccess.DatabasePlatform;

/**
 * INTERNAL:
 * <p><b>Purpose</b>:LOBValueWriter is used to write a large size of object into an Oracle
 * CLOB/BLOB column through Oracle LOB Locator. It's a work-around object for the well-known 4k write
 * limits on an Oracle thin driver.
 *
 * <p><b>Responsibilities</b>:<ul>
 * <li> Build the Oracle empty lob method call string for the insert call.
 * <li> Build the minimal SELECT call to retrieve the locator.
 * <li> Write the lob value through the locator.
 * <li> Resolve the multiple table INSERT/SELECT orders.
 * <li> Resolve the nested unit of work commit issue.
 * </ul>
 *
 * @author: King Wang
 * @since TopLink/Java 5.0. July 2002.
 */
public class LOBValueWriter {
    //DatabaseCalls still to be processed
    private Collection calls = null;
    private Accessor accessor;
    private boolean isNativeConnectionRequired;

    /**
     * This is the default constructor for the class.
     *
     * Bug 2804663 - Each DatabaseAccessor will now hold on to its own instance
     * of this class, hence a singleton pattern is not applicable.
     */
    public LOBValueWriter(Accessor accessor) {
        this.accessor = accessor;
        DatabasePlatform platform = ((DatabaseAccessor)accessor).getPlatform();
        this.isNativeConnectionRequired = platform.isOracle() && ((OraclePlatform)platform).isNativeConnectionRequiredForLobLocator();
    }

    protected void buildAndExecuteCall(DatabaseCall dbCall, AbstractSession session) {
        DatabaseQuery query = dbCall.getQuery();
        if (!query.isWriteObjectQuery()) {
            //if not writequery, should not go through the locator writing..
            return;
        }
        WriteObjectQuery writeQuery = (WriteObjectQuery)query;
        writeQuery.setAccessor(accessor);
        //build a select statement form the query
        SQLSelectStatement selectStatement = buildSelectStatementForLocator(writeQuery, dbCall, session);

        //then build a call from the statement
        DatabaseCall call = buildCallFromSelectStatementForLocator(selectStatement, writeQuery, dbCall, session);

        accessor.executeCall(call, call.getQuery().getTranslationRow(), session);
    }

    /**
    * Fetch the locator(s) from the result set and write LOB value to the table
    */
    public void fetchLocatorAndWriteValue(DatabaseCall dbCall, Object resultSet) throws SQLException {
        Enumeration enumFields = dbCall.getContexts().getFields().elements();
        Enumeration enumValues = dbCall.getContexts().getValues().elements();
        AbstractSession executionSession = dbCall.getQuery().getSession().getExecutionSession(dbCall.getQuery());
        while (enumFields.hasMoreElements()) {
            DatabaseField field = (DatabaseField)enumFields.nextElement();
            Object value = enumValues.nextElement();

            //write the value through the locator
            executionSession.getPlatform().writeLOB(field, value, (ResultSet)resultSet, executionSession);
        }
    }

    /**
    * Build the select statement for selecting the locator
    */
    private SQLSelectStatement buildSelectStatementForLocator(WriteObjectQuery writeQuery, DatabaseCall call, AbstractSession session) {
        SQLSelectStatement selectStatement = new SQLSelectStatement();
        Vector tables = writeQuery.getDescriptor().getTables();
        selectStatement.setTables(tables);
        //rather than get ALL fields from the descriptor, only use the LOB-related fields to build the minimal SELECT statement.
        selectStatement.setFields(call.getContexts().getFields());
        //the where clause setting here is sufficient if the object does not map to multiple tables.
        selectStatement.setWhereClause(writeQuery.getDescriptor().getObjectBuilder().buildPrimaryKeyExpressionFromObject(writeQuery.getObject(), session));
        //need pessimistic locking for the locator select
        selectStatement.setLockingClause(ForUpdateClause.newInstance(ObjectBuildingQuery.LOCK));

        if (tables.size() > 1) {
            //the primary key expression from the primary table
            Expression expression = selectStatement.getWhereClause();

            //additional join from the non-primary tables
            Expression additionalJoin = writeQuery.getDescriptor().getQueryManager().getAdditionalJoinExpression();
            if (additionalJoin != null) {
                expression = expression.and(additionalJoin);
            }

            //where clause now contains extra joins across all tables
            selectStatement.setWhereClause(expression);
        }

        //normalize the statement at the end, such as assign alias to all tables, and build sorting statement
        selectStatement.normalize(session, writeQuery.getDescriptor());
        return selectStatement;
    }

    /**
    * Build the sql call from the select statement for selecting the locator
    */
    private DatabaseCall buildCallFromSelectStatementForLocator(SQLSelectStatement selectStatement, WriteObjectQuery writeQuery, DatabaseCall dbCall, AbstractSession session) {
        DatabaseCall call = selectStatement.buildCall(session);
        // Locator LOB must not be wrapped (WLS wraps LOBs).
        call.setIsNativeConnectionRequired(this.isNativeConnectionRequired);

        //the LOB context must be passed into the new call object
        call.setContexts(dbCall.getContexts());
        //need to explicitly define one row return, otherwise, EL assumes multiple rows return and confuses the accessor
        call.returnOneRow();
        //the query object has to be set in order to access to the platform and login objects
        call.setQuery(writeQuery);
        // prepare it
        call.prepare(session);
        //finally do the translation
        call.translate(writeQuery.getTranslationRow(), writeQuery.getModifyRow(), session);
        return call;
    }

    // Building of SELECT statements is no longer done in DatabaseAccessor.basicExecuteCall
    // for updates because DatabaseCall.isUpdateCall() can't recognize update in case
    // StoredProcedureCall is used. Therefore in all cases: insert(single or multiple tables)
    // and update the original (insert and update) calls are saved
    // and both building and executing of SELECT statements postponed until
    // buildAndExecuteSelectCalls method is called.

    /**
    * Add original (insert or update) call to the collection
    */
    public void addCall(Call call) {
        if (calls == null) {
            //use lazy initialization
            calls = new ArrayList(2);
        }
        calls.add(call);
    }

    // Bug 3110860: RETURNINGPOLICY-OBTAINED PK CAUSES LOB TO BE INSERTED INCORRECTLY
    // The deferred locator SELECT calls should be generated and executed after ReturningPolicy
    // merges PK obtained from the db into the object held by the query.
    // That's why original (insert or update) calls are saved,
    // and both building and executing of SELECT statements postponed until
    // this method is called.

    /**
    * Build and execute the deferred select calls.
    */
    public void buildAndExecuteSelectCalls(AbstractSession session) {
        if ((calls == null) || calls.isEmpty()) {
            //no deferred select calls (it means no locator is required)
            return;
        }

        //all INSERTs have been executed, time to execute the SELECTs
        try {
            for (Iterator callIt = calls.iterator(); callIt.hasNext();) {
                DatabaseCall dbCall = (DatabaseCall)callIt.next();
                buildAndExecuteCall(dbCall, session);
            }
        } finally {
            //after executing all select calls, need to empty the collection.
            //this is necessary in the nested unit of work cases.
            calls.clear();
        }
    }
}
