// Copyright (c) 2007, Oracle. All rights reserved.  
package oracle.toplink.essentials.extension.spatial;

import java.io.Writer;
import java.sql.*;
import oracle.spatial.geometry.JGeometry;
import oracle.sql.STRUCT;
import oracle.toplink.essentials.internal.databaseaccess.BindCallCustomParameter;
import oracle.toplink.essentials.internal.helper.DatabaseField;
import oracle.toplink.essentials.internal.sessions.AbstractSession;
import oracle.toplink.essentials.platform.database.oracle.OraclePlatform;
import oracle.toplink.essentials.queryframework.Call;


/**
 * This custom database platform extends the base OraclePlatform shipped in
 * TopLink essentials providing support for reading and writing Oracle Spatial
 * oracle.spatial.geometry.JGeometry types. These types are generally mapped to
 * MDSYS.SDO_GEOMETRY columns
 *
 * @author Doug Clarke
 * @since TopLink Essentials v2b41
 */
public class Oracle10SpatialPlatform extends OraclePlatform {
    private static final Class ORACLE_STRUCT = STRUCT.class;
    private static final String JGEOMETRY_DB_TYPE = "MDSYS.SDO_GEOMETRY";
    private static final Class JGEOMETRY_CLASS = JGeometry.class;

    /**
     * After retrieving the object from the ResultSet the geometry STRUCT's are
     * identified and immediately converted into JGeometry instances. This must
     * be done here as an active JDBC connection is required.
     */
    public Object getObjectFromResultSet(ResultSet resultSet, int columnNumber,
                                         int type) throws SQLException {
        Object result =
            super.getObjectFromResultSet(resultSet, columnNumber, type);

        if (result != null && type == Types.STRUCT &&
            result.getClass() == ORACLE_STRUCT &&
            ((STRUCT)result).getDescriptor().getSQLName().getName().equalsIgnoreCase(JGEOMETRY_DB_TYPE)) {
            return JGeometry.load((STRUCT)result);
        }

        return result;
    }

    /**
     * Overriding this method allows the platform to perform special processing
     * of types. In this case the usage of JGeometry types requires the values
     * to always be bound. In the case of a NULL JGeometry instance a special
     * place-holder (NullSTRUCT) is used. This ensure that the null value is
     * properly bound into the JDBC statement.
     *
     * @return the value or a custom modify call
     */
    public Object getCustomModifyValueForCall(Call call, Object value,
                                              DatabaseField field,
                                              boolean shouldBind) {
        if (field.getType() == JGEOMETRY_CLASS) {
            Object bindValue = value;
            if (bindValue == null) {
                bindValue = new NullSTRUCT(JGEOMETRY_DB_TYPE);
            }
            return new BindCallCustomParameter(bindValue);
        }

        return super.getCustomModifyValueForCall(call, value, field,
                                                 shouldBind);
    }

    /**
     * Determine if a custom modify call should be used for a specific database
     * field.
     *
     * @see #getCustomModifyValueForCall
     */
    public boolean shouldUseCustomModifyForCall(DatabaseField field) {
        return field.getType() == JGEOMETRY_CLASS ||
            super.shouldUseCustomModifyForCall(field);
    }

    /**
     * When binding values into a PreparedStatement this method allows a custom
     * platform to provide special type support. Here the NullSTRUCT and
     * JGeometry->Struct binding logic is supported.
     */
    public void setParameterValueInDatabaseCall(Object parameter,
                                                PreparedStatement statement,
                                                int index,
                                                AbstractSession session) throws SQLException {
        Object param = parameter;

        if (param != null && parameter.getClass() == NullSTRUCT.class) {
            ((NullSTRUCT)param).setNull(statement, index);
            return;
        }

        if (param != null && parameter.getClass() == JGEOMETRY_CLASS) {
            param =
                    JGeometry.store((JGeometry)parameter, statement.getConnection());
        }

        super.setParameterValueInDatabaseCall(param, statement, index,
                                              session);
    }

    /**
     * When TopLink Essentials is generating SQL the platform can provide custom
     * formatting for various types. Here the JGeometry instances being written
     * are forced into a binding scenario instead.
     */
    public void appendParameter(Call call, Writer writer, Object parameter) {
        Object param = parameter;

        if (param != null && param.getClass() == JGEOMETRY_CLASS) {
            param = new BindCallCustomParameter(param);
        }
        super.appendParameter(call, writer, param);
    }

    /**
     * Place holder for null be bound for a STRUCT column. By default TopLink
     * binds all null values as a String. This fails for STRUCT columns. This
     * placeholder is used to wrap the database column type so that it can also
     * be used in the statement.setNull call.
     */
    private class NullSTRUCT {
        String dbType;

        NullSTRUCT(String dbTypeName) {
            this.dbType = dbTypeName;
        }

        void setNull(PreparedStatement statement,
                     int index) throws SQLException {
            statement.setNull(index, Types.STRUCT, this.dbType);
        }

        public String toString() {
            return "NULL (" + this.dbType + ")";
        }
    }
}