/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: DTXQuery.java,v 1.2 2005/03/05 13:41:49 mfuchs Exp $
 */

package org.exolab.castor.dtx;

import java.io.*;
import java.util.*;
import java.sql.*;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributeListImpl;
import org.exolab.castor.jdo.*;
import org.exolab.castor.mapping.xml.ClassMapping;
import org.exolab.castor.mapping.xml.FieldMapping;
import org.exolab.castor.mapping.xml.Sql;
import org.exolab.castor.mapping.xml.BindXml;
import org.exolab.castor.mapping.xml.MapTo;
import org.exolab.castor.persist.spi.PersistenceFactory;
import org.exolab.castor.persist.spi.QueryExpression;

/**
 * A single query that takes OQL query language, reads data from an
 * RDBMS, and returns the results as SAX events. The query can be
 * parameterized, and be re-used with different bound values.
 *
 * There are currently some severe limitations on the contents of the
 * OQL query. The query needs to be a SELECT and must return a single
 * class.  However, it can handle multiple returned objects, which are
 * sent to the DocumentHandler as multiple documents.
 *
 * DTXQuery's can't be created on their own; use the DTXEngine to
 * prepare a new query.
 *
 * An additional note: the bind() methods take the parameters in order
 * that they appear, NOT using the number value of the parameter
 * name. So, if you have a query like:
 *
 * <pre>... WHERE foo.id = $2 AND bar.name = $1 ... </pre>
 *
 * then the statement bind(1, 12) will bind the value 12 to the first
 * parameter, even though it's named "$2." This may or may not be
 * changed in future releases.
 *
 * @author <a href="0@intalio.com">Evan Prodromou</a>
 * @version $Revision: 1.2 $ $Date: 2005/03/05 13:41:49 $
 */

public class DTXQuery {

    protected DTXEngine _eng = null;
    protected DocumentHandler _handler = null;
    protected PrintWriter _logWriter = null;
    protected PreparedStatement _stmt = null;
    protected String _objName = null;
    protected String _objType = null;
    protected ClassMapping _clsMapping = null;
    protected ArrayList _ids = null;
    protected int _lastCol = 0;
    protected HashMap _cols = null;
    protected HashMap _classes = null;

    /**
     * Set the DocumentHandler that will receive the results (as SAX
     * events) for this query. By default, the query will use the
     * document handler of its DTXEngine.
     *
     * Changing the DocumentHandler in the middle of a query is
     * ill-advised.
     *
     * @param handler The DocumentHandler to use.
     */

    public void setHandler(DocumentHandler handler) {
	_handler = handler;
    }

    /**
     * Set the log writer.
     *
     * @param logWriter The log writer to use.
     */

    public void setLogWriter(PrintWriter logWriter) {
	_logWriter = logWriter;
    }

    /**
     * Binds an Object value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value Object to bind.
     */

    public void bind(int param, Object value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setObject(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds an String value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value String to bind.
     */

    public void bind(int param, String value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setString(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds an integer value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value int to bind.
     */

    public void bind(int param, int value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setInt(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds a long integer value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value long integer to bind.
     */

    public void bind(int param, long value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setLong(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds a float value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value float to bind.
     */

    public void bind(int param, float value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setFloat(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds a double value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value double to bind.
     */

    public void bind(int param, double value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setDouble(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * Binds a boolean value to a parameter in the query.
     *
     * @param param 1-based index of the param (see note above).
     * @param value boolean to bind.
     */

    public void bind(int param, boolean value) throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    _stmt.setBoolean(param, value);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /**
     * This method executes the query. All results of the query are
     * sent to the DocumentHandler specified by setHandler() as SAX
     * events.
     */

    public void execute() throws DTXException {
	if (_stmt == null) {
	    throw new DTXException("No prepared statement.");
	}

	try {
	    ResultSet rs = _stmt.executeQuery();
	    emitSaxEvents(rs);
	    rs.close();
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /* Package scope */

    /* Default do-nothing constructor. */

    DTXQuery() {
    }

    /* Sets the engine to use for getting connections and
       descriptors. */

    void setEngine(DTXEngine eng) {
	_eng = eng;
    }

    /* Prepares this query for use. Parses the OQL into SQL (plus some
       residual information), connects to the database and prepares a
       SQL statement. */

    void prepare(String oql) throws DTXException {
	try {
	    String sql = parseOQL(oql);
	    Connection conn = _eng.getConnection();
	    _stmt = conn.prepareStatement(sql);
	} catch (SQLException sqle) {
	    throw new DTXException(sqle);
	}
    }

    /* protected */

    /* Translates a JDBC resultset into SAX events. Uses the recursive
       helper to get started. */

    protected void emitSaxEvents(ResultSet rs) throws DTXException {

	try {
	    if (rs.next()) {
		emitSaxInt(rs, 0);
	    }
	} catch (Exception e) {
	    e.printStackTrace(_logWriter);
	    throw new DTXException(e);
	}
    }

    /* Recursive SAX emitter. Emits top-level elements, attributes,
       and simple (non-object) elements, then descends recursively
       into lower and lower child elements.

       This code is -highly- loopy, and it makes some assumptions
       about the ordering of sub-elements that may be unacceptable.
    */

    protected boolean emitSaxInt(ResultSet rs, int idIndex) throws DTXException {

	boolean hasValue = true;

	try {
	    String initParentValue = null;
	    String parentValue = null;
	    int parentColNum = 0;

	    if (idIndex != 0) {
		String parentCol = (String) _ids.get(idIndex - 1);
		Integer parentColInt = (Integer) _cols.get(parentCol);
		parentColNum = parentColInt.intValue();
		initParentValue = rs.getString(parentColNum);
		parentValue = initParentValue;
	    }

	    String idCol = (String) _ids.get(idIndex);
	    Integer idColNum = (Integer) _cols.get(idCol);

	    DTXClassDescriptor desc = (DTXClassDescriptor) _classes.get(idCol);
	    ClassMapping clsMapping = desc.getClassMapping();
	    String elementName = null;

	    if (clsMapping.getMapTo() == null ||
		clsMapping.getMapTo().getXml() == null) {
		elementName = clsMapping.getName();
	    } else {
		elementName = clsMapping.getMapTo().getXml();
	    }

	    String[] attrCols = desc.getAttrCols();
	    String[] simpleElementCols = desc.getSimpleElementCols();
	    String textCol = desc.getTextCol();

	    while (hasValue && (idIndex == 0 || parentValue.equalsIgnoreCase(initParentValue))) {
		if (idIndex == 0) {
		    _handler.startDocument();
		}

		String idVal = rs.getString(idColNum.intValue());

		AttributeListImpl attrs = new AttributeListImpl();

		for (int i = 0; i < attrCols.length; i++) {
		    String attrCol = attrCols[i];
		    Integer attrColNum = (Integer) _cols.get(attrCol);
		    FieldMapping field = desc.getAttr(attrCol);
		    String attrName = null;
		    if (field.getBindXml() == null || field.getBindXml().getName() == null) {
			attrName = field.getName();
		    } else {
			attrName = field.getBindXml().getName();
		    }

		    String attrValue = rs.getString(attrColNum.intValue());
		    attrs.addAttribute(attrName, "CDATA", attrValue);
		}

		_handler.startElement(elementName, attrs);

		for (int j = 0; j < simpleElementCols.length; j++) {
		    String simpleElementCol = simpleElementCols[j];
		    Integer elementColNum = (Integer) _cols.get(simpleElementCol);
		    FieldMapping elField = desc.getSimpleElement(simpleElementCol);
		    String elName = null;
		    if (elField.getBindXml() == null || elField.getBindXml().getName() == null) {
			elName = elField.getName();
		    } else {
			elName = elField.getBindXml().getName();
		    }

		    String elValue = rs.getString(elementColNum.intValue());
		    _handler.startElement(elName, new AttributeListImpl());
		    _handler.characters(elValue.toCharArray(), 0, elValue.length());
		    _handler.endElement(elName);
		}

		if (idIndex < _ids.size() - 1) {
		    hasValue = emitSaxInt(rs, idIndex + 1);
		}

		if (textCol != null) {
		    Integer textColNum = (Integer) _cols.get(textCol);
		    String textColValue = rs.getString(textColNum.intValue());
		    _handler.characters(textColValue.toCharArray(), 0, textColValue.length());
		}

		_handler.endElement(elementName);

		if (idIndex == 0) {
		    _handler.endDocument();
		}

		String newIdVal = rs.getString(idColNum.intValue());

		// Check to see if sub-call advanced this for us.

		if (newIdVal.equalsIgnoreCase(idVal)) {
		    hasValue = rs.next();
		}
		if (hasValue) {
		    if (idIndex != 0) {
			parentValue = rs.getString(parentColNum);
		    }
		}
	    }
	} catch (Exception e) {
	    e.printStackTrace(_logWriter);
	    throw new DTXException(e);
	}

	return hasValue;
    }

    /* OQL parser. Most of this code is highjacked from the JDO
       OQLQueryImpl, with a lot of changes to keep from using
       JDOClassDescriptors, which depend on loading a Java class.

       It'd be nice to refactor this so there's only one OQL-parsing
       class.
    */

    protected String parseOQL(String oql) throws DTXException {

	try {
	    _ids = new ArrayList();
	    _cols = new HashMap();
	    _classes = new HashMap();

	    StringTokenizer token = new StringTokenizer(oql);

	    if (! token.hasMoreTokens() || ! token.nextToken().equalsIgnoreCase("SELECT"))
		throw new DTXException("Query must start with SELECT");
	    if (! token.hasMoreTokens())
		throw new DTXException("Missing object name");
	    _objName = token.nextToken();
	    if (! token.hasMoreTokens() || ! token.nextToken().equalsIgnoreCase("FROM"))
		throw new DTXException("Object must be followed by FROM");
	    if (! token.hasMoreTokens())
		throw new DTXException("Missing object type");
	    _objType = token.nextToken();
	    if (! token.hasMoreTokens())
		throw new DTXException("Missing object name");
	    if (! _objName.equals(token.nextToken()))
		throw new DTXException("Object name not same in SELECT and FROM");

	    if (_logWriter != null) {
		_logWriter.println("Querying " + _objName + " of type " + _objType);
	    }

	    _clsMapping = _eng.getClassMapping(_objType);

	    if (_clsMapping == null) {
		throw new DTXException("dtx.NoClassDescription: " + _objType);
	    }

	    PersistenceFactory factory = _eng.getFactory();

	    if (factory == null) {
		throw new DTXException("dtx.NoFactory");
	    }

	    QueryExpression expr = factory.getQueryExpression();

	    if (expr == null) {
		throw new DTXException("dtx.NoQueryExpression");
	    }

	    initQuery(_clsMapping, expr);

	    if (token.hasMoreTokens()) {
		if (! token.nextToken().equalsIgnoreCase("WHERE")) {
		    throw new DTXException("Missing WHERE clause");
		}
		addField(_clsMapping, token, expr);
		while (token.hasMoreTokens()) {
		    if (!token.nextToken().equals("AND")) {
			throw new QueryException( "Only AND supported in WHERE clause" );
		    }
		    addField(_clsMapping, token, expr);
		}
	    }

	    String sql = expr.getStatement(false);

	    sql = sql + " ORDER BY ";

	    for (java.util.Iterator it = _ids.iterator(); it.hasNext(); ) {
		String id = (String) it.next();
		sql = sql + " " + id;
		if (it.hasNext()) {
		    sql = sql + ",";
		}
	    }

	    if (_logWriter != null) {
		_logWriter.println("SQL: " + sql);
	    }

	    return sql;

	} catch (Exception e) {
	    if (_logWriter != null) {
		e.printStackTrace(_logWriter);
	    }
	    throw new DTXException(e);
	}
    }


    /* This makes the basic parts of a query. Again, a lot of code was
       lifted from e.g. SQLEngine, and edited to use *Mapping instead
       of *Descriptor. It also should probably be re-factored
       somewhere.

       Additionally, contained ("rel") elements go through the same
       kind of manipulation as the main class; this stuff should be
       re-factored, too.
    */

    protected void initQuery(ClassMapping clsMapping, QueryExpression expr)
        throws DTXException
    {
        ClassMapping extend;
	MapTo mapTo = clsMapping.getMapTo();

	if (mapTo == null) {
	    throw new DTXException("no table mapping for: " + clsMapping.getName());
	}

	String table = mapTo.getTable();

        FieldMapping[] fields = clsMapping.getFieldMapping();
	FieldMapping identity = null;

        String identityName = clsMapping.getIdentity(0);

	for (int j = 0; j < fields.length; j++) {
	    if (fields[j].getName().equals(identityName)) {
		identity = fields[j];
		break;
	    }
	}

	if (identity == null) {
	    throw new DTXException("no identity field in class: " + clsMapping.getName());
	}

	Sql identitySQLElement = identity.getSql();

	if (identitySQLElement == null) {
	    throw new DTXException("no identity SQL info in class: " + clsMapping.getName());
	}

	String identitySQL = identitySQLElement.getName()[0];

	_ids.add(table + "." + identitySQL);

	DTXClassDescriptor desc = new DTXClassDescriptor(clsMapping);

	_classes.put(table + "." + identitySQL, desc);

        // If this class extends another class, create a join with the parent table and
        // add the load fields of the parent class (but not the store fields)
        if (clsMapping.getExtends() != null) {

        /**
          * TODO : Needs to be resolved by Hand
          */

        MapTo extendsTo = new MapTo();//(ClassMapping) clsMapping.getExtends()).getMapTo();
	    if (extendsTo == null) {
		throw new DTXException("no mapping info for extends table.");
	    }
	    String extendsTable = extendsTo.getTable();
            expr.addInnerJoin(table, identitySQL, extendsTable, identitySQL);
            /**
             * needs to be resolved by hand
             */
            initQuery(new ClassMapping(), expr);
            //(ClassMapping) clsMapping.getExtends(), expr);
        }

        for (int i = 0; i < fields.length; ++i) {
	    FieldMapping field = fields[i];
	    Sql fieldSql = field.getSql();
	    ClassMapping relMapping = _eng.getClassMapping(field.getType());

	    if (fieldSql == null) {
		if (relMapping != null) {

		    // We have a one-to-many relationship with a sub object.
		    // get those objects, too.

                    FieldMapping[] relFields = relMapping.getFieldMapping();
		    MapTo relMapTo = relMapping.getMapTo();

		    if (relMapTo == null) {
			throw new DTXException("dtx.NoRelatedMapTo");
		    }

		    String relTable = relMapTo.getTable();

		    String relId = null;
                    String foreKey = null;

                    for (int k = 0; k < relFields.length; k++ ) {
			Sql relSql = relFields[k].getSql();
                        if (relSql != null) {
			    String type = relFields[k].getType();
			    if (type != null && type.equals(clsMapping.getName())) {
				foreKey = relSql.getName()[0];
			    }
			}
                    }

                    if (foreKey != null) {
                        expr.addOuterJoin(table, identitySQL, relTable, foreKey);
			DTXClassDescriptor relDesc = new DTXClassDescriptor(relMapping);

			for (int n = 0; n < relFields.length; n++ ) {
			    FieldMapping relField = relFields[n];
			    Sql relSql = relFields[n].getSql();
			    if (relSql != null) {
				String relFieldName = relSql.getName()[0];
				if (relFieldName == null) {
				    relFieldName = relFields[n].getName();
				}
				String relFullName = relTable + "." + relFieldName;

				BindXml relFieldXml = relFields[n].getBindXml();
				String node = "element";
				if (relFieldXml != null) {
				    node = relFieldXml.getNode().toString();
				}

				if (!relFieldName.equals(foreKey)) {
				    expr.addColumn(relTable, relFieldName);
				    _cols.put(relTable + "." + relFieldName, new Integer(++_lastCol));

				    if (node.equalsIgnoreCase("attribute")) {
					relDesc.addAttr(relFullName, relField);
				    } else if (node.equalsIgnoreCase("element")) {
					relDesc.addSimpleElement(relFullName, relField);
				    } else if (node.equalsIgnoreCase("text")) {
					relDesc.setTextCol(relFullName, relField);
				    }
				}
				if (relField.getName().equals(relMapping.getIdentity())) {
				    _ids.add(relTable + "." + relFieldName);
				    desc.addContained(relTable + "." + relFieldName, relMapping);
				    _classes.put(relTable + "." + relFieldName, relDesc);
				}
			    }
			}
                    }
                }

	    } else {

		String fieldName = fieldSql.getName()[0];
		if (fieldName == null) {
		    fieldName = fields[i].getName();
		}
		String fullName = table + "." + fieldName;

		BindXml fieldXml = field.getBindXml();
		String node = "element";
		if (fieldXml != null) {
		     node = fieldXml.getNode().toString();
		}

		if (node.equalsIgnoreCase("attribute")) {
		    desc.addAttr(fullName, field);
		} else if (node.equalsIgnoreCase("element")) {
		    desc.addSimpleElement(fullName, field);
		} else if (node.equalsIgnoreCase("text")) {
		    desc.setTextCol(fullName, field);
		}

		if (relMapping == null || fieldSql.getManyTable() == null) {
		    expr.addColumn(table, fieldName);
		    _cols.put(fullName, new Integer(++_lastCol));
		} else {
		    expr.addColumn(fieldSql.getManyTable(),
				   fieldName);
		    _cols.put(fullName, new Integer(++_lastCol));
		    expr.addOuterJoin(table, identitySQL,
				      fieldSql.getManyTable(),
				      fieldSql.getManyKey()[0]);
		}
	    }
        }
    }

    /* A misnomer, addField() actually adds a condition. Lifted, once
       again, from JDO stuff. */

    private void addField(ClassMapping clsMapping, StringTokenizer token, QueryExpression expr)
        throws DTXException
    {
        if (! token.hasMoreTokens()) {
            throw new DTXException("Missing field name");
	}
        String name = token.nextToken();
        if (! token.hasMoreTokens()) {
            throw new DTXException("Missing operator");
	}
        String op = token.nextToken();
        if (! token.hasMoreTokens())
            throw new DTXException("Missing field value");

        String value = token.nextToken();
        if (name.indexOf(".") > 0) {
            name = name.substring(name.indexOf(".") + 1);
	}

        FieldMapping[] fields = clsMapping.getFieldMapping();
	FieldMapping field = null;

        for (int i = 0; i < fields.length; ++i) {
            if (fields[i].getSql() != null && fields[i].getName().equals(name)) {
                field = fields[i];
                break;
            }
        }

        if (field == null) {
            throw new DTXException("The field " + name + " was not found");
	}

	Sql fieldSql = field.getSql();
	String table = clsMapping.getMapTo().getTable();

        if (value.startsWith("$")) {
            expr.addParameter(table, fieldSql.getName()[0], op);
        } else {
            expr.addCondition(table, fieldSql.getName()[0], op, value);
        }
    }
}
