org.exist.xquery.modules.oracle.ExecuteFunction.java Source code

Java tutorial

Introduction

Here is the source code for org.exist.xquery.modules.oracle.ExecuteFunction.java

Source

/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2010 The eXist Project
 *  http://exist-db.org
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  $Id$
 */
package org.exist.xquery.modules.oracle;

import java.io.PrintStream;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLRecoverableException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import oracle.jdbc.OracleTypes;

import org.apache.log4j.Logger;
import org.exist.Namespaces;
import org.exist.dom.QName;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.exist.memtree.MemTreeBuilder;
import org.exist.xquery.BasicFunction;
import org.exist.xquery.Cardinality;
import org.exist.xquery.FunctionSignature;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.modules.sql.SQLModule;
import org.exist.xquery.modules.sql.SQLUtils;
import org.exist.xquery.value.BooleanValue;
import org.exist.xquery.value.FunctionParameterSequenceType;
import org.exist.xquery.value.FunctionReturnSequenceType;
import org.exist.xquery.value.IntegerValue;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.Type;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * eXist Oracle Module Extension ExecuteFunction
 * 
 * Execute a PL/SQL stored procedure within an Oracle RDBMS.
 * 
 * @author Robert Walpole <robert.walpole@metoffice.gov.uk>
 * @serial 2009-03-23
 * @version 1.0
 * 
 * @see org.exist.xquery.BasicFunction#BasicFunction(org.exist.xquery.XQueryContext,
 *      org.exist.xquery.FunctionSignature)
 */
public class ExecuteFunction extends BasicFunction {

    private static final Logger LOG = Logger.getLogger(ExecuteFunction.class);

    final static FunctionSignature[] signatures = { new FunctionSignature(
            new QName("execute", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
            "Executes a PL/SQL stored procedure passed as the second argument against an Oracle RDBMS specified by the connection "
                    + "in the first argument with the position of the result set cursor at the fourth argument. Stored procedure parameters "
                    + "may be passed in the third argument using an XML fragment with the following structure: "
                    + "<oracle:parameters><orace:param oracle:pos=\"{param-position}\" oracle:type=\"{param-type}\"/>{param-value}"
                    + "</oracle:parameters>.",
            new SequenceType[] {
                    new FunctionParameterSequenceType("connection-handle", Type.INTEGER, Cardinality.EXACTLY_ONE,
                            "The connection handle"),
                    new FunctionParameterSequenceType("plsql-statement", Type.STRING, Cardinality.EXACTLY_ONE,
                            "The PL/SQL stored procedure"),
                    new FunctionParameterSequenceType("parameters", Type.ELEMENT, Cardinality.ZERO_OR_ONE,
                            "Input parameters for the stored procedure (if any)"),
                    new FunctionParameterSequenceType("result-set-position", Type.INTEGER, Cardinality.ZERO_OR_ONE,
                            "The position of the result set cursor"),
                    new FunctionParameterSequenceType("make-node-from-column-name", Type.BOOLEAN,
                            Cardinality.EXACTLY_ONE,
                            "The flag that indicates whether " + ""
                                    + "the xml nodes should be formed from the column names (in this mode a space in a Column Name will be replaced by an underscore!)") },
            new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the results")),
            new FunctionSignature(new QName("execute", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                    "Executes a PL/SQL stored procedure passed as the second argument against an Oracle RDBMS specified by the connection "
                            + "in the first argument with the position of the result set cursor at the fourth argument. Stored procedure parameters "
                            + "may be passed in the third argument using an XML fragment with the following structure: "
                            + "<oracle:parameters><orace:param oracle:pos=\"{param-position}\" oracle:type=\"{param-type}\"/>{param-value}"
                            + "</oracle:parameters>. An additional return code parameter is supported which can be used to specify an integer value returned "
                            + "in the first position of the statement to indicate success of the PL/SQL call.",
                    new SequenceType[] {
                            new FunctionParameterSequenceType("connection-handle", Type.INTEGER,
                                    Cardinality.EXACTLY_ONE, "The connection handle"),
                            new FunctionParameterSequenceType("plsql-statement", Type.STRING,
                                    Cardinality.EXACTLY_ONE, "The PL/SQL stored procedure"),
                            new FunctionParameterSequenceType("parameters", Type.ELEMENT, Cardinality.ZERO_OR_ONE,
                                    "Input parameters for the stored procedure (if any)"),
                            new FunctionParameterSequenceType("result-set-position", Type.INTEGER,
                                    Cardinality.EXACTLY_ONE, "The position of the result set cursor"),
                            new FunctionParameterSequenceType("make-node-from-column-name", Type.BOOLEAN,
                                    Cardinality.EXACTLY_ONE,
                                    "The flag that indicates whether "
                                            + "the xml nodes should be formed from the column names (in this mode a space in a Column Name will be replaced by an underscore!)"),
                            new FunctionParameterSequenceType("return-code", Type.INTEGER, Cardinality.EXACTLY_ONE,
                                    "The expected function return code which indicates successful execution") },
                    new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the results")) };

    private final static String PARAMETERS_ELEMENT_NAME = "parameters";
    private final static String PARAM_ELEMENT_NAME = "param";
    private final static String TYPE_ATTRIBUTE_NAME = "type";
    private final static String POSITION_ATTRIBUTE_NAME = "pos";

    private DateFormat xmlDf;

    /**
      * ExecuteFunction Constructor
      *
      * @param context
      *            The Context of the calling XQuery
      */
    public ExecuteFunction(XQueryContext context, FunctionSignature signature) {
        super(context, signature);
        xmlDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
    }

    @Override
    public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException {

        if (args.length == 5 || args.length == 6) {
            // was a connection and PL/SQL statement specified?
            if (args[0].isEmpty() || args[1].isEmpty()) {
                return (Sequence.EMPTY_SEQUENCE);
            }

            // get the Connection
            long connectionUID = ((IntegerValue) args[0].itemAt(0)).getLong();
            Connection connection = SQLModule.retrieveConnection(context, connectionUID);

            if (connection == null) {
                return (Sequence.EMPTY_SEQUENCE);
            }

            // get the PL/SQL statement
            String plSql = args[1].getStringValue();

            // get the input parameters (if any)
            Element parameters = null;
            if (!args[2].isEmpty()) {
                parameters = (Element) args[2].itemAt(0);
            }

            // was a result set position specified?
            int resultSetPos = 0;
            if (!args[3].isEmpty()) {
                resultSetPos = ((IntegerValue) args[3].itemAt(0)).getInt();
            }

            boolean haveReturnCode = false;
            int plSqlSuccess = 1; // default value of 1 for success
            if (args.length == 6) {
                // a return code is expected so what is the value indicating success?
                plSqlSuccess = ((IntegerValue) args[5].itemAt(0)).getInt();
                haveReturnCode = true;
            }

            CallableStatement statement = null;
            ResultSet resultSet = null;

            try {
                MemTreeBuilder builder = context.getDocumentBuilder();
                int iRow = 0;

                statement = connection.prepareCall(plSql);
                if (haveReturnCode) {
                    statement.registerOutParameter(1, Types.NUMERIC);
                }
                if (resultSetPos != 0) {
                    statement.registerOutParameter(resultSetPos, OracleTypes.CURSOR);
                }
                if (!args[2].isEmpty()) {
                    setParametersOnPreparedStatement(statement, parameters);
                }

                statement.execute();

                if (haveReturnCode) {
                    int returnCode = statement.getInt(1);
                    if (returnCode != plSqlSuccess) {
                        LOG.error(plSql + " failed [" + returnCode + "]");
                        return (Sequence.EMPTY_SEQUENCE);
                    }
                }

                if (resultSetPos != 0) {
                    // iterate through the result set building an XML document
                    builder.startDocument();

                    builder.startElement(new QName("result", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                            null);
                    builder.addAttribute(new QName("count", null, null), String.valueOf(-1));

                    resultSet = (ResultSet) statement.getObject(resultSetPos);

                    ResultSetMetaData rsmd = resultSet.getMetaData();
                    int iColumns = rsmd.getColumnCount();

                    while (resultSet.next()) {
                        builder.startElement(new QName("row", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                                null);
                        builder.addAttribute(new QName("index", null, null), String.valueOf(resultSet.getRow()));

                        // get each tuple in the row
                        for (int i = 0; i < iColumns; i++) {
                            String columnName = rsmd.getColumnLabel(i + 1);
                            if (columnName != null) {
                                String colValue = resultSet.getString(i + 1);

                                String colElement = "field";

                                if (((BooleanValue) args[4].itemAt(0)).effectiveBooleanValue()
                                        && columnName.length() > 0) {
                                    // use column names as the XML node

                                    /**
                                     * Spaces in column names are replaced with
                                     * underscore's
                                     */

                                    colElement = SQLUtils.escapeXmlAttr(columnName.replace(' ', '_'));
                                }

                                builder.startElement(
                                        new QName(colElement, OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                                        null);

                                if (!((BooleanValue) args[4].itemAt(0)).effectiveBooleanValue()
                                        || columnName.length() <= 0) {
                                    String name;

                                    if (columnName.length() > 0) {
                                        name = SQLUtils.escapeXmlAttr(columnName);
                                    } else {
                                        name = "Column: " + String.valueOf(i + 1);
                                    }

                                    builder.addAttribute(new QName("name", null, null), name);
                                }

                                builder.addAttribute(
                                        new QName("type", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                                        rsmd.getColumnTypeName(i + 1));
                                builder.addAttribute(new QName("type", Namespaces.SCHEMA_NS, "xs"),
                                        Type.getTypeName(SQLUtils.sqlTypeToXMLType(rsmd.getColumnType(i + 1))));

                                if (resultSet.wasNull()) {
                                    // Add a null indicator attribute if the value was SQL Null
                                    builder.addAttribute(
                                            new QName("null", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                                            "true");
                                }

                                if (colValue != null) {
                                    builder.characters(SQLUtils.escapeXmlText(colValue));
                                }

                                builder.endElement();
                            }
                        }

                        builder.endElement();

                        iRow++;
                    }
                    builder.endElement();

                    // Change the root element count attribute to have the correct value

                    NodeValue node = (NodeValue) builder.getDocument().getDocumentElement();

                    Node count = node.getNode().getAttributes().getNamedItem("count");

                    if (count != null) {
                        count.setNodeValue(String.valueOf(iRow));
                    }
                    builder.endDocument();

                    // return the XML result set
                    return (node);
                } else {
                    // there was no result set so just return an empty sequence
                    return (Sequence.EMPTY_SEQUENCE);
                }
            } catch (SQLException sqle) {

                LOG.error("oracle:execute() Caught SQLException \"" + sqle.getMessage() + "\" for PL/SQL: \""
                        + plSql + "\"", sqle);

                //return details about the SQLException
                MemTreeBuilder builder = context.getDocumentBuilder();

                builder.startDocument();
                builder.startElement(new QName("exception", OracleModule.NAMESPACE_URI, OracleModule.PREFIX), null);

                boolean recoverable = false;
                if (sqle instanceof SQLRecoverableException) {
                    recoverable = true;
                }
                builder.addAttribute(new QName("recoverable", null, null), String.valueOf(recoverable));

                builder.startElement(new QName("state", OracleModule.NAMESPACE_URI, OracleModule.PREFIX), null);
                String sqlState = sqle.getSQLState();
                if (sqlState != null) {
                    builder.characters(sqle.getSQLState());
                } else {
                    builder.characters("null");
                }

                builder.endElement();

                builder.startElement(new QName("message", OracleModule.NAMESPACE_URI, OracleModule.PREFIX), null);
                builder.characters(sqle.getMessage());
                builder.endElement();

                builder.startElement(new QName("stack-trace", OracleModule.NAMESPACE_URI, OracleModule.PREFIX),
                        null);
                ByteArrayOutputStream bufStackTrace = new ByteArrayOutputStream();
                sqle.printStackTrace(new PrintStream(bufStackTrace));
                builder.characters(new String(bufStackTrace.toByteArray()));
                builder.endElement();

                builder.startElement(new QName("oracle", OracleModule.NAMESPACE_URI, OracleModule.PREFIX), null);
                builder.characters(SQLUtils.escapeXmlText(plSql));
                builder.endElement();

                int line = getLine();
                int column = getColumn();

                builder.startElement(new QName("xquery", OracleModule.NAMESPACE_URI, OracleModule.PREFIX), null);
                builder.addAttribute(new QName("line", null, null), String.valueOf(line));
                builder.addAttribute(new QName("column", null, null), String.valueOf(column));
                builder.endElement();

                builder.endElement();
                builder.endDocument();

                return (NodeValue) builder.getDocument().getDocumentElement();
            } finally {
                release(connection, statement, resultSet);
            }
        } else {
            throw new XPathException("Invalid number of arguments [" + args.length + "]");
        }
    }

    /**
     * Release DB resources
     * @param connection
     * @param statement
     * @param rs
     */
    protected void release(Connection connection, Statement statement, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException sqle) {
                LOG.error("Unable to close ResultSet: ", sqle);
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException sqle) {
                LOG.error("Unable to close Statement: ", sqle);
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException sqle) {
                LOG.error("Unable to close Connection: ", sqle);
            }
        }
    }

    private void setParametersOnPreparedStatement(Statement stmt, Element parametersElement)
            throws SQLException, XPathException {

        if (parametersElement.getNamespaceURI().equals(OracleModule.NAMESPACE_URI)
                && parametersElement.getLocalName().equals(PARAMETERS_ELEMENT_NAME)) {
            NodeList paramElements = parametersElement.getElementsByTagNameNS(OracleModule.NAMESPACE_URI,
                    PARAM_ELEMENT_NAME);

            for (int i = 0; i < paramElements.getLength(); i++) {
                Element param = ((Element) paramElements.item(i));
                String value = param.getFirstChild().getNodeValue();
                String type = param.getAttributeNS(OracleModule.NAMESPACE_URI, TYPE_ATTRIBUTE_NAME);
                int position = Integer
                        .parseInt(param.getAttributeNS(OracleModule.NAMESPACE_URI, POSITION_ATTRIBUTE_NAME));
                try {
                    int sqlType = SQLUtils.sqlTypeFromString(type);
                    // What if SQL type is date???
                    if (sqlType == Types.DATE) {
                        Date date = xmlDf.parse(value);
                        ((PreparedStatement) stmt).setTimestamp(position, new Timestamp(date.getTime()));
                    } else {
                        ((PreparedStatement) stmt).setObject(position, value, sqlType);
                    }
                } catch (ParseException pex) {
                    throw new XPathException(this, "Unable to parse date from value " + value
                            + ". Expected format is YYYY-MM-DDThh:mm:ss.sss");
                } catch (Exception ex) {
                    throw new XPathException(this, "Failed to set stored procedure parameter at position "
                            + position + " as " + type + " with value " + value);
                }
            }
        }
    }

}