/*******************************************************************************
 * Copyright (c) 1998, 2016 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 v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 *     Vikram Bhatia - added method for releasing temporary LOBs after conversion
 ******************************************************************************/
package org.eclipse.persistence.platform.database.oracle;

import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Struct;
import java.util.Hashtable;

import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.FieldTypeDefinition;
import org.eclipse.persistence.internal.databaseaccess.Platform;
import org.eclipse.persistence.internal.databaseaccess.SimpleAppendCallCustomParameter;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.platform.database.OraclePlatform;
import org.eclipse.persistence.queries.Call;

import oracle.jdbc.OracleBlob;
import oracle.jdbc.OracleClob;


/**
 * <p><b>Purpose:</b>
 * Supports certain new Oracle 8 data types, and usage of certain Oracle JDBC specific APIs.
 * <p> Supports Oracle thin JDBC driver LOB &gt;4k binding workaround.
 * <p> Creates BLOB and CLOB type for byte[] and char[] for table creation.
 * <p> Supports object-relational data-type creation.
 */
public class Oracle8Platform extends OraclePlatform {

    /** Locator is required for Oracle thin driver to write LOB value exceeds the limits */
    protected boolean usesLocatorForLOBWrite = true;

    /** The LOB value limits when the Locator is required for the writing */
    protected int lobValueLimits = 0;

    /**
     * INTERNAL:
     */
    @Override
    protected Hashtable buildFieldTypes() {
        Hashtable fieldTypeMapping = super.buildFieldTypes();

        fieldTypeMapping.put(Byte[].class, new FieldTypeDefinition("BLOB", false));
        fieldTypeMapping.put(Character[].class, new FieldTypeDefinition("CLOB", false));

        return fieldTypeMapping;
    }

    /**
     * INTERNAL:
     * Allow for conversion from the Oralce type to the Java type.
     */
    @Override
    public void copyInto(Platform platform) {
        super.copyInto(platform);
        if (!(platform instanceof Oracle8Platform)) {
            return;
        }
        Oracle8Platform oracle8Platform = (Oracle8Platform)platform;
        oracle8Platform.setShouldUseLocatorForLOBWrite(shouldUseLocatorForLOBWrite());
        oracle8Platform.setLobValueLimits(getLobValueLimits());
    }

    /**
     * INTERNAL:
     * Return if the LOB value size is larger than the limit, i.e. 4k.
     */
    protected boolean lobValueExceedsLimit(Object value) {
        if (value == null) {
            return false;
        }
        int limit = getLobValueLimits();
        if (value instanceof byte[]) {//blob
            return ((byte[])value).length >= limit;
        } else if (value instanceof String) {//clob
            return ((String)value).length() >= limit;
        } else {
            return false;
        }
    }

    /**
     * INTERNAL:
     * This method is used to unwrap the oracle connection wrapped by
     * the application server.  TopLink needs this unwrapped connection for certain
     * Oracle Specific support. (ie TIMESTAMPTZ, LOB)
     * This is added as a workaround for bug 4565190
     */
    @Override
    public Connection getConnection(AbstractSession session, Connection connection) {
        if (session.getServerPlatform() != null && (session.getLogin()).shouldUseExternalConnectionPooling()){
        // This is added as a workaround for bug 4460996
            return session.getServerPlatform().unwrapConnection(connection);
        }
        return connection;
    }

    /**
     * INTERNAL
     * Used by SQLCall.translate(..)
     * Typically there is no field translation (and this is default implementation).
     * However on different platforms (Oracle) there are cases such that the values for
     * binding and appending may be different (BLOB, CLOB).
     * In these special cases the method returns a wrapper object
     * which knows whether it should be bound or appended and knows how to do that.
     */
    @Override
    public Object getCustomModifyValueForCall(Call call, Object value, DatabaseField field, boolean shouldBind) {
        Class type = field.getType();
        if (ClassConstants.BLOB.equals(type) || ClassConstants.CLOB.equals(type)) {
            if(value == null) {
                return null;
            }
            value = convertToDatabaseType(value);
            if (shouldUseLocatorForLOBWrite()) {
                if (lobValueExceedsLimit(value)) {
                    ((DatabaseCall)call).addContext(field, value);
                    if (ClassConstants.BLOB.equals(type)) {
                        if (shouldBind) {
                            value = new byte[1];
                        } else {
                            value = new SimpleAppendCallCustomParameter("empty_blob()");
                        }
                    } else {
                        if (shouldBind) {
                            value = new String(" ");
                        } else {
                            value = new SimpleAppendCallCustomParameter("empty_clob()");
                        }
                    }
                }
            }
            return value;
        }
        return super.getCustomModifyValueForCall(call, value, field, shouldBind);
    }

    /**
     * INTERNAL
     * Used by SQLCall.appendModify(..)
     * If the field should be passed to customModifyInDatabaseCall, retun true,
     * otherwise false.
     * Methods shouldCustomModifyInDatabaseCall and customModifyInDatabaseCall should be
     * kept in sync: shouldCustomModifyInDatabaseCall should return true if and only if the field
     * is handled by customModifyInDatabaseCall.
     */
    @Override
    public boolean shouldUseCustomModifyForCall(DatabaseField field) {
        if (shouldUseLocatorForLOBWrite()) {
            Class type = field.getType();
            if (ClassConstants.BLOB.equals(type) || ClassConstants.CLOB.equals(type)) {
                return true;
            }
        }
        return super.shouldUseCustomModifyForCall(field);
    }

    /**
     * INTERNAL:
     * Write LOB value - works on Oracle 10 and newer
     */
    @Override
    public void writeLOB(DatabaseField field, Object value, ResultSet resultSet, AbstractSession session) throws SQLException {
        if (isBlob(field.getType())) {
            //change for 338585 to use getName instead of getNameDelimited
            Blob blob = (Blob) resultSet.getObject(field.getName());
            blob.setBytes(1, (byte[]) value);
            //impose the localization
            session.log(SessionLog.FINEST, SessionLog.SQL, "write_BLOB", Long.valueOf(blob.length()), field.getName());
        } else if (isClob(field.getType())) {
            //change for 338585 to use getName instead of getNameDelimited
            Clob clob = (Clob) resultSet.getObject(field.getName());
            clob.setString(1, (String) value);
            //impose the localization
            session.log(SessionLog.FINEST, SessionLog.SQL, "write_CLOB", Long.valueOf(clob.length()), field.getName());
        } else {
            //do nothing for now, open to BFILE or NCLOB types
        }
    }

    /**
     * INTERNAL:
     * Used in writeLOB method only to identify a BLOB
     */
    protected boolean isBlob(Class type) {
        return ClassConstants.BLOB.equals(type);
    }

    /**
     * INTERNAL:
     * Used in writeLOB method only to identify a CLOB
     */
    protected boolean isClob(Class type) {
        return ClassConstants.CLOB.equals(type);
    }

    /**
     * INTERNAL:
     * Indicates whether app. server should unwrap connection
     * to use lob locator.
     */
    @Override
    public boolean isNativeConnectionRequiredForLobLocator() {
        return true;
    }

    /**
     * PUBLIC:
     * Set if the locator is required for the LOB write. The default is true.
     * For Oracle thin driver, the locator is recommended for large size
     * ( &gt;4k for Oracle8, &gt;5.9K for Oracle9) BLOB/CLOB value write.
     */
    public void setShouldUseLocatorForLOBWrite(boolean usesLocatorForLOBWrite) {
        this.usesLocatorForLOBWrite = usesLocatorForLOBWrite;
    }

    /**
     * PUBLIC:
     * Return if the locator is required for the LOB write. The default is true.
     * For Oracle thin driver, the locator is recommended for large size
     * ( &gt;4k for Oracle8, &gt;5.9K for Oracle9) BLOB/CLOB value write.
     */
    public boolean shouldUseLocatorForLOBWrite() {
        return usesLocatorForLOBWrite;
    }

    /**
     * PUBLIC:
     * Return the BLOB/CLOB value limits on thin driver. The default value is 0.
     * If usesLocatorForLOBWrite is true, locator will be used in case the
     * lob's size is larger than lobValueLimit.
     */
    public int getLobValueLimits() {
        return lobValueLimits;
    }

    /**
     * PUBLIC:
     * Set the BLOB/CLOB value limits on thin driver. The default value is 0.
     * If usesLocatorForLOBWrite is true, locator will be used in case the
     * lob's size is larger than lobValueLimit.
     */
    public void setLobValueLimits(int lobValueLimits) {
        this.lobValueLimits = lobValueLimits;
    }

    /**
     * INTERNAL:
     * Platforms that support java.sql.Array may override this method.
     * @return Array
     */
    @Override
    public Array createArray(String elementDataTypeName, Object[] elements, Connection connection) throws SQLException {
        return new oracle.sql.ARRAY(new oracle.sql.ArrayDescriptor(elementDataTypeName, connection), connection, elements);
    }

    /**
     * INTERNAL:
     * Platforms that support java.sql.Struct may override this method.
     * @return Struct
     */
    @Override
    public Struct createStruct(String structTypeName, Object[] attributes, Connection connection) throws SQLException {
        return new oracle.sql.STRUCT(new oracle.sql.StructDescriptor(structTypeName, connection), connection, attributes);
    }

    /**
     * INTERNAL:
     * Overrides DatabasePlatform method.
     * @return String
     */
    @Override
    public Object getRefValue(Ref ref,Connection connection) throws SQLException {
        ((oracle.sql.REF)ref).setPhysicalConnectionOf(connection);
        return ((oracle.sql.REF)ref).getValue();
    }

    /**
     * INTERNAL:
     * Used by Oracle platforms during reading of ResultSet to free temporary LOBs.
     */
    @Override
    public void freeTemporaryObject(Object value) throws SQLException {
        if (value instanceof OracleClob && ((OracleClob) value).isTemporary()) {
            ((OracleClob) value).free();
        } else if (value instanceof OracleBlob && ((OracleBlob) value).isTemporary()) {
            ((OracleBlob) value).free();
        }
    }
}
