/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.cayenne.dba.oracle;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.access.DataNode;
import org.apache.cayenne.access.OperationObserver;
import org.apache.cayenne.access.trans.QualifierTranslator;
import org.apache.cayenne.access.trans.QueryAssembler;
import org.apache.cayenne.access.trans.TrimmingQualifierTranslator;
import org.apache.cayenne.access.types.ByteArrayType;
import org.apache.cayenne.access.types.ByteType;
import org.apache.cayenne.access.types.CharType;
import org.apache.cayenne.access.types.DefaultType;
import org.apache.cayenne.access.types.ExtendedTypeMap;
import org.apache.cayenne.access.types.ShortType;
import org.apache.cayenne.dba.JdbcAdapter;
import org.apache.cayenne.dba.PkGenerator;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.query.BatchQuery;
import org.apache.cayenne.query.InsertBatchQuery;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SQLAction;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.query.UpdateBatchQuery;
import org.apache.cayenne.util.Util;

/**
 * DbAdapter implementation for <a href="http://www.oracle.com">Oracle RDBMS </a>. Sample
 * <a target="_top" href="../../../../../../../developerguide/unit-tests.html">connection
 * settings </a> to use with Oracle are shown below:
 * 
 * <pre>
 *       
 *        
 *         test-oracle.cayenne.adapter = org.apache.cayenne.dba.oracle.OracleAdapter
 *         test-oracle.jdbc.username = test
 *         test-oracle.jdbc.password = secret
 *         test-oracle.jdbc.url = jdbc:oracle:thin:@192.168.0.20:1521:ora1 
 *         test-oracle.jdbc.driver = oracle.jdbc.driver.OracleDriver
 *         
 *        
 * </pre>
 * 
 * @author Andrei Adamchik
 */
public class OracleAdapter extends JdbcAdapter {

    private static Logger logObj = Logger.getLogger(OracleAdapter.class);

    public static final String ORACLE_FLOAT = "FLOAT";
    public static final String ORACLE_BLOB = "BLOB";
    public static final String ORACLE_CLOB = "CLOB";

    public static final String TRIM_FUNCTION = "RTRIM";
    public static final String NEW_CLOB_FUNCTION = "EMPTY_CLOB()";
    public static final String NEW_BLOB_FUNCTION = "EMPTY_BLOB()";

    protected static boolean initDone;
    protected static int oracleCursorType = Integer.MAX_VALUE;
    protected static Method outputStreamFromBlobMethod;
    protected static Method writerFromClobMethod;
    protected static boolean supportsOracleLOB;

    static {
        // TODO: as CAY-234 shows, having such initialization done in a static fashion
        // makes it untestable and any potential problems hard to reproduce. Make this
        // an instance method (with all the affected vars) and write unit tests.
        initDriverInformation();
    }

    protected static void initDriverInformation() {
        initDone = true;

        // configure static information
        try {
            Class oraTypes = Class.forName("oracle.jdbc.driver.OracleTypes");
            Field cursorField = oraTypes.getField("CURSOR");
            oracleCursorType = cursorField.getInt(null);

            outputStreamFromBlobMethod = Class.forName("oracle.sql.BLOB").getMethod(
                    "getBinaryOutputStream",
                    new Class[0]);

            writerFromClobMethod = Class.forName("oracle.sql.CLOB").getMethod(
                    "getCharacterOutputStream",
                    new Class[0]);
            supportsOracleLOB = true;

        }
        catch (Throwable th) {
            logObj.debug("Error getting Oracle driver information, ignoring. "
                    + "Note that certain adapter features will be disabled.", Util
                    .unwindException(th));
        }
    }

    public static Method getOutputStreamFromBlobMethod() {
        return outputStreamFromBlobMethod;
    }

    // TODO: rename to something that looks like English ...
    public static boolean isSupportsOracleLOB() {
        return supportsOracleLOB;
    }

    /**
     * Utility method that returns <code>true</code> if the query will update at least
     * one BLOB or CLOB DbAttribute.
     * 
     * @since 1.2
     */
    static boolean updatesLOBColumns(BatchQuery query) {
        boolean isInsert = query instanceof InsertBatchQuery;
        boolean isUpdate = query instanceof UpdateBatchQuery;

        if (!isInsert && !isUpdate) {
            return false;
        }

        List updatedAttributes = (isInsert)
                ? query.getDbAttributes()
                : ((UpdateBatchQuery) query).getUpdatedAttributes();

        Iterator it = updatedAttributes.iterator();
        while (it.hasNext()) {
            int type = ((DbAttribute) it.next()).getType();
            if (type == Types.CLOB || type == Types.BLOB) {
                return true;
            }
        }

        return false;
    }

    public static Method getWriterFromClobMethod() {
        return writerFromClobMethod;
    }

    /**
     * Returns an Oracle JDBC extension type defined in
     * oracle.jdbc.driver.OracleTypes.CURSOR. This value is determined from Oracle driver
     * classes via reflection in runtime, so that Cayenne code has no compile dependency
     * on the driver. This means that calling this method when the driver is not available
     * will result in an exception.
     */
    public static int getOracleCursorType() {

        if (oracleCursorType == Integer.MAX_VALUE) {
            throw new CayenneRuntimeException(
                    "No information exists about oracle types. "
                            + "Check that Oracle JDBC driver is available to the application.");
        }

        return oracleCursorType;
    }

    public OracleAdapter() {
        // enable batch updates by default
        setSupportsBatchUpdates(true);
    }

    /**
     * Installs appropriate ExtendedTypes as converters for passing values between JDBC
     * and Java layers.
     */
    protected void configureExtendedTypes(ExtendedTypeMap map) {
        super.configureExtendedTypes(map);

        // create specially configured CharType handler
        map.registerType(new CharType(true, true));

        // create specially configured ByteArrayType handler
        map.registerType(new ByteArrayType(true, true));

        // override date handler with Oracle handler
        map.registerType(new OracleUtilDateType());

        // At least on MacOS X, driver does not handle Short and Byte properly
        map.registerType(new ShortType(true));
        map.registerType(new ByteType(true));

        // these two types are needed to replace PreparedStatement binding
        // via "setObject()" to a call to setInt or setDouble to make
        // Oracle happy, esp. with the AST* expression classes
        // that do not evaluate constants to BigDecimals, but rather
        // Integer and Double
        map.registerType(new OracleIntegerType());
        map.registerType(new OracleDoubleType());
    }

    /**
     * Creates and returns a primary key generator. Overrides superclass implementation to
     * return an instance of OraclePkGenerator.
     */
    protected PkGenerator createPkGenerator() {
        return new OraclePkGenerator();
    }

    /**
     * Returns a query string to drop a table corresponding to <code>ent</code>
     * DbEntity. Changes superclass behavior to drop all related foreign key constraints.
     */
    public String dropTable(DbEntity ent) {
        return "DROP TABLE " + ent.getFullyQualifiedName() + " CASCADE CONSTRAINTS";
    }

    /**
     * Fixes some reverse engineering problems. Namely if a columns is created as DECIMAL
     * and has non-positive precision it is converted to INTEGER.
     */
    public DbAttribute buildAttribute(
            String name,
            String typeName,
            int type,
            int size,
            int precision,
            boolean allowNulls) {

        DbAttribute attr = super.buildAttribute(
                name,
                typeName,
                type,
                size,
                precision,
                allowNulls);

        if (type == Types.DECIMAL && precision <= 0) {
            attr.setType(Types.INTEGER);
            attr.setPrecision(-1);
        }
        else if (type == Types.OTHER) {
            // in this case we need to guess the attribute type
            // based on its string value
            if (ORACLE_FLOAT.equals(typeName)) {
                attr.setType(Types.FLOAT);
            }
            else if (ORACLE_BLOB.equals(typeName)) {
                attr.setType(Types.BLOB);
            }
            else if (ORACLE_CLOB.equals(typeName)) {
                attr.setType(Types.CLOB);
            }
        }
        else if (type == Types.DATE) {
            // Oracle DATE can store JDBC TIMESTAMP
            if ("DATE".equals(typeName)) {
                attr.setType(Types.TIMESTAMP);
            }
        }

        return attr;
    }

    /**
     * Returns Oracle-specific translator for object SELECT queries.
     * 
     * @deprecated Since 1.2 this is done via custom SQLActions.
     */
    protected Class queryTranslatorClass(Query q) {
        if (q instanceof SelectQuery) {
            return OracleSelectTranslator.class;
        }
        else {
            return super.queryTranslatorClass(q);
        }
    }

    /**
     * Returns a trimming translator.
     */
    public QualifierTranslator getQualifierTranslator(QueryAssembler queryAssembler) {
        return new TrimmingQualifierTranslator(
                queryAssembler,
                OracleAdapter.TRIM_FUNCTION);
    }

    /**
     * Uses OracleActionBuilder to create the right action.
     * 
     * @since 1.2
     */
    public SQLAction getAction(Query query, DataNode node) {
        return query.createSQLAction(new OracleActionBuilder(this, node
                .getEntityResolver()));
    }

    /**
     * Implements special LOB handling in batches.
     * 
     * @deprecated Since 1.2
     */
    public boolean shouldRunBatchQuery(
            DataNode node,
            Connection con,
            BatchQuery query,
            OperationObserver delegate) throws SQLException, Exception {

        // special handling for LOB updates
        if (isSupportsOracleLOB()
                && OracleAdapter.updatesLOBColumns(query)
                && (node instanceof OracleDataNode)) {

            OracleDataNode oracleNode = (OracleDataNode) node;
            oracleNode.runBatchUpdateWithLOBColumns(con, query, delegate);

            return false;
        }
        else {
            return super.shouldRunBatchQuery(node, con, query, delegate);
        }
    }

    /**
     * @since 1.1
     */
    final class OracleIntegerType extends DefaultType {

        public OracleIntegerType() {
            super(Integer.class.getName());
        }

        public void setJdbcObject(
                PreparedStatement st,
                Object val,
                int pos,
                int type,
                int precision) throws Exception {

            if (val != null) {
                st.setInt(pos, ((Number) val).intValue());
            }
            else {
                super.setJdbcObject(st, val, pos, type, precision);
            }
        }
    }

    /**
     * @since 1.1
     */
    final class OracleDoubleType extends DefaultType {

        public OracleDoubleType() {
            super(Double.class.getName());
        }

        public void setJdbcObject(
                PreparedStatement st,
                Object val,
                int pos,
                int type,
                int precision) throws Exception {

            if (val != null) {
                st.setDouble(pos, ((Number) val).doubleValue());
            }
            else {
                super.setJdbcObject(st, val, pos, type, precision);
            }
        }
    }
}
