| /* |
| * 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.cocoon.transformation; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.lang.reflect.Field; |
| import java.sql.CallableStatement; |
| import java.sql.Clob; |
| import java.sql.Connection; |
| import java.sql.DriverManager; |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.ResultSetMetaData; |
| import java.sql.SQLException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TreeMap; |
| |
| import org.apache.avalon.excalibur.datasource.DataSourceComponent; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.ServiceSelector; |
| import org.apache.excalibur.xml.sax.SAXParser; |
| |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.components.sax.XMLDeserializer; |
| import org.apache.cocoon.components.sax.XMLSerializer; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.transformation.helpers.TextRecorder; |
| import org.apache.cocoon.xml.IncludeXMLConsumer; |
| |
| import org.apache.commons.lang.StringEscapeUtils; |
| import org.apache.commons.lang.StringUtils; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| /** |
| * The <code>SQLTransformer</code> can be plugged into a pipeline to transform |
| * SAX events into updated or queries and responses to/from a SQL interface. |
| * |
| * <p> |
| * It is declared and configured as follows: |
| * <pre> |
| * <map:transformers default="..."> |
| * <map:transformer name="sql" src="org.apache.cocoon.transformation.SQLTransformer"> |
| * <old-driver>false</old-driver> |
| * <connection-attempts>5</connection-attempts> |
| * <connection-waittime>5000</connection-waittime> |
| * </map:transformer> |
| * </map:transformers> |
| * </pre> |
| * </p> |
| * |
| * <p> |
| * It can be used in the sitemap pipeline as follows: |
| * <code> |
| * <map:transform type="sql"> |
| * <!-- True to force each query to create its own connection: --> |
| * <map:parameter name="own-connection" value="..."/> |
| * <!-- Specify either name of datasource: --> |
| * <map:parameter name="use-connection" value="..."/> |
| * <!-- Or connection parameters: --> |
| * <map:parameter name="dburl" value="..."/> |
| * <map:parameter name="username" value="..."/> |
| * <map:parameter name="password" value="..."/> |
| * |
| * <!-- Default query parameters: --> |
| * <map:parameter name="show-nr-or-rows" value="false"/> |
| * <map:parameter name="doc-element" value="rowset"/> |
| * <map:parameter name="row-element" value="row"/> |
| * <map:parameter name="namespace-uri" value="http://apache.org/cocoon/SQL/2.0"/> |
| * <map:parameter name="namespace-prefix" value="sql"/> |
| * <map:parameter name="clob-encoding" value=""/> |
| * </map:transform> |
| * </pre> |
| * </p> |
| * |
| * <p> |
| * The following DTD is valid: |
| * <code> |
| * <!ENTITY % param "(own-connection?,(use-connection|(dburl,username,password))?,show-nr-or-rows?,doc-element?,row-element?,namespace-uri?,namespace-prefix?,clob-encoding?)"><br> |
| * <!ELEMENT execute-query (query,(in-parameter|in-xml-parameter|out-parameter)*,execute-query?, %param;)><br> |
| * <!ELEMENT own-connection (#PCDATA)><br> |
| * <!ELEMENT use-connection (#PCDATA)><br> |
| * <!ELEMENT query (#PCDATA | substitute-value | ancestor-value | escape-string| xml)*><br> |
| * <!ATTLIST query name CDATA #IMPLIED isstoredprocedure (true|false) "false" isupdate (true|false) "false"><br> |
| * <!ELEMENT substitute-value EMPTY><br> |
| * <!ATTLIST substitute-value name CDATA #REQUIRED><br> |
| * <!ELEMENT ancestor-value EMPTY><br> |
| * <!ATTLIST ancestor-value name CDATA #REQUIRED level CDATA #REQUIRED><br> |
| * <!ELEMENT in-parameter EMPTY><br> |
| * <!ATTLIST in-parameter nr CDATA #REQUIRED type CDATA #REQUIRED><br> |
| * <!ELEMENT in-xml-parameter EMPTY><br> |
| * <!ATTLIST in-xml-parameter nr CDATA #REQUIRED type CDATA #REQUIRED><br> |
| * <!ELEMENT out-parameter EMPTY><br> |
| * <!ATTLIST out-parameter nr CDATA #REQUIRED name CDATA #REQUIRED type CDATA #REQUIRED><br> |
| * <!ELEMENT escape-string (#PCDATA)><br> |
| * <!ELEMENT xml (#PCDATA)><br> |
| * </code> |
| * </p> |
| * |
| * <p> |
| * Each query can override default transformer parameters. Nested queries do not inherit parent |
| * query parameters, but only transformer parameters. Each query can have connection to different |
| * database, directly or using the connection pool. If database connection parameters are the same |
| * as for any of the ancestor queries, nested query will re-use ancestor query connection. |
| * </p> |
| * |
| * <p> |
| * Connection sharing between queries can be disabled, globally or on per-query basis, using |
| * <code>own-connection</code> parameter. |
| * </p> |
| * |
| * <p> |
| * By default, CLOBs are read from the database using getSubString, so that character |
| * decoding is performed by the database. Using <code>clob-encoding</code> parameter, |
| * this behavior can be overrided, so that data is read as byte stream and decoded using |
| * specified character encoding. |
| * </p> |
| * |
| * <p> |
| * Inserting of XML data can be done by using the new sql:xml or SQL:in-xml-parameter tags. |
| * - sql:xml must be used like sql:escape-string |
| * - sql:in-xml-parameter must be used like sql:in-parameter. |
| * </p> |
| * |
| * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a> |
| * @author <a href="mailto:balld@webslingerZ.com">Donald Ball</a> |
| * @author <a href="mailto:giacomo.pati@pwr.ch">Giacomo Pati</a> |
| * (PWR Organisation & Entwicklung) |
| * @author <a href="mailto:sven.beauprez@the-ecorp.com">Sven Beauprez</a> |
| * @author <a href="mailto:a.saglimbeni@pro-netics.com">Alfio Saglimbeni</a> |
| * @author <a href="mailto:pmhahn@titan.lahn.de">Philipp Hahn</a> |
| * @author <a href="mailto:vgritsenko@apache.org">Vadim Gritsenko</a> |
| * @version $Id$ |
| */ |
| public class SQLTransformer extends AbstractSAXTransformer { |
| |
| private static final int BUFFER_SIZE = 1024; |
| |
| /** The SQL transformer namespace */ |
| public static final String NAMESPACE = "http://apache.org/cocoon/SQL/2.0"; |
| |
| // The SQL trasformer namespace element names |
| public static final String MAGIC_EXECUTE_QUERY = "execute-query"; |
| private static final String MAGIC_OWN_CONNECTION = "own-connection"; |
| public static final String MAGIC_CONNECTION = "use-connection"; |
| public static final String MAGIC_DBURL = "dburl"; |
| public static final String MAGIC_USERNAME = "username"; |
| public static final String MAGIC_PASSWORD = "password"; |
| public static final String MAGIC_PROP = "prop"; |
| public static final String MAGIC_NR_OF_ROWS = "show-nr-of-rows"; |
| public static final String MAGIC_QUERY = "query"; |
| public static final String MAGIC_VALUE = "value"; |
| public static final String MAGIC_COLUMN_CASE = "column-case"; |
| public static final String MAGIC_DOC_ELEMENT = "doc-element"; |
| public static final String MAGIC_ROW_ELEMENT = "row-element"; |
| public static final String MAGIC_IN_PARAMETER = "in-parameter"; |
| public static final String MAGIC_IN_PARAMETER_NR_ATTRIBUTE = "nr"; |
| public static final String MAGIC_IN_PARAMETER_VALUE_ATTRIBUTE = "value"; |
| public static final String MAGIC_OUT_PARAMETER = "out-parameter"; |
| public static final String MAGIC_OUT_PARAMETER_NAME_ATTRIBUTE = "name"; |
| public static final String MAGIC_OUT_PARAMETER_NR_ATTRIBUTE = "nr"; |
| public static final String MAGIC_OUT_PARAMETER_TYPE_ATTRIBUTE = "type"; |
| public static final String MAGIC_ESCAPE_STRING = "escape-string"; |
| public static final String MAGIC_XML = "xml"; |
| public static final String MAGIC_IN_XML_PARAMETER = "in-xml-parameter"; |
| public static final String MAGIC_ERROR = "error"; |
| |
| public static final String MAGIC_NS_URI_ELEMENT = "namespace-uri"; |
| public static final String MAGIC_NS_PREFIX_ELEMENT = "namespace-prefix"; |
| |
| public static final String MAGIC_ANCESTOR_VALUE = "ancestor-value"; |
| public static final String MAGIC_ANCESTOR_VALUE_LEVEL_ATTRIBUTE = "level"; |
| public static final String MAGIC_ANCESTOR_VALUE_NAME_ATTRIBUTE = "name"; |
| public static final String MAGIC_SUBSTITUTE_VALUE = "substitute-value"; |
| public static final String MAGIC_SUBSTITUTE_VALUE_NAME_ATTRIBUTE = "name"; |
| public static final String MAGIC_NAME_ATTRIBUTE = "name"; |
| public static final String MAGIC_STORED_PROCEDURE_ATTRIBUTE = "isstoredprocedure"; |
| public static final String MAGIC_UPDATE_ATTRIBUTE = "isupdate"; |
| public static final String CLOB_ENCODING = "clob-encoding"; |
| |
| // The states we are allowed to be in |
| protected static final int STATE_OUTSIDE = 0; |
| protected static final int STATE_INSIDE_EXECUTE_QUERY_ELEMENT = 1; |
| protected static final int STATE_INSIDE_VALUE_ELEMENT = 2; |
| protected static final int STATE_INSIDE_QUERY_ELEMENT = 3; |
| protected static final int STATE_INSIDE_ANCESTOR_VALUE_ELEMENT = 4; |
| protected static final int STATE_INSIDE_SUBSTITUTE_VALUE_ELEMENT = 5; |
| protected static final int STATE_INSIDE_IN_PARAMETER_ELEMENT = 6; |
| protected static final int STATE_INSIDE_OUT_PARAMETER_ELEMENT = 7; |
| protected static final int STATE_INSIDE_ESCAPE_STRING = 8; |
| protected static final int STATE_INSIDE_XML = 9; |
| protected static final int STATE_INSIDE_IN_XML_PARAMETER_ELEMENT = 10; |
| |
| // |
| // Configuration |
| // |
| |
| /** Is the old-driver turned on? (default is off) */ |
| protected boolean oldDriver; |
| |
| /** How many connection attempts to do? (default is 5 times) */ |
| protected int connectAttempts; |
| |
| /** How long wait between connection attempts? (default is 5000 ms) */ |
| protected int connectWaittime; |
| |
| // |
| // State |
| // |
| |
| /** The current query we are working on */ |
| protected Query query; |
| |
| /** The current state of the event receiving FSM */ |
| protected int state; |
| |
| /** The datasource component selector */ |
| protected ServiceSelector datasources; |
| |
| /** The "name" of the connection shared by top level queries (if configuration allows) */ |
| protected String connName; |
| |
| /** The connection shared by top level queries (if configuration allows) */ |
| protected Connection conn; |
| |
| // Used to parse XML from database. |
| protected XMLSerializer compiler; |
| protected XMLDeserializer interpreter; |
| protected SAXParser parser; |
| |
| /** |
| * Constructor |
| */ |
| public SQLTransformer() { |
| super.defaultNamespaceURI = NAMESPACE; |
| } |
| |
| // |
| // Lifecycle Methods |
| // |
| |
| /** |
| * Serviceable |
| */ |
| public void service(ServiceManager manager) throws ServiceException { |
| super.service(manager); |
| try { |
| this.datasources = (ServiceSelector) manager.lookup(DataSourceComponent.ROLE + "Selector"); |
| } catch (ServiceException e) { |
| getLogger().warn("DataSource component selector is not available.", e); |
| } |
| } |
| |
| /** |
| * Configure transformer. Supported configuration elements: |
| * <ul> |
| * <li>old-driver</li> |
| * <li>connect-attempts</li> |
| * <li>connect-waittime</li> |
| * </ul> |
| */ |
| public void configure(Configuration conf) throws ConfigurationException { |
| super.configure(conf); |
| |
| this.oldDriver = conf.getChild("old-driver").getValueAsBoolean(false); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Value for old-driver is " + this.oldDriver); |
| } |
| |
| this.connectAttempts = conf.getChild("connect-attempts").getValueAsInteger(5); |
| this.connectWaittime = conf.getChild("connect-waittime").getValueAsInteger(5000); |
| } |
| |
| /** |
| * Setup for the current request. |
| */ |
| public void setup(SourceResolver resolver, Map objectModel, |
| String source, Parameters parameters) |
| throws ProcessingException, SAXException, IOException { |
| super.setup(resolver, objectModel, source, parameters); |
| |
| // Setup instance variables |
| this.state = SQLTransformer.STATE_OUTSIDE; |
| this.connName = name(super.parameters); |
| } |
| |
| /** |
| * Recycle this component |
| */ |
| public void recycle() { |
| this.query = null; |
| try { |
| // Close the connection used by all top level queries |
| if (this.conn != null) { |
| this.conn.close(); |
| this.conn = null; |
| } |
| } catch (SQLException e) { |
| getLogger().info("Could not close connection", e); |
| } |
| this.connName = null; |
| |
| this.manager.release(this.parser); |
| this.parser = null; |
| this.manager.release(this.compiler); |
| this.compiler = null; |
| this.manager.release(this.interpreter); |
| this.interpreter = null; |
| |
| super.recycle(); |
| } |
| |
| /** |
| * Dispose |
| */ |
| public void dispose() { |
| if (this.datasources != null) { |
| this.manager.release(this.datasources); |
| this.datasources = null; |
| } |
| super.dispose(); |
| } |
| |
| /** |
| * Return attribute value. |
| * First try non-namespaced attribute, then try this transformer namespace. |
| * @param name local attribute name |
| */ |
| private String getAttributeValue(Attributes attr, String name) { |
| String value = attr.getValue("", name); |
| if (value == null) { |
| value = attr.getValue(this.namespaceURI, name); |
| } |
| |
| return value; |
| } |
| |
| // |
| // SAX Events Handlers |
| // |
| |
| protected static void throwIllegalStateException(String message) { |
| throw new IllegalStateException("Illegal state: " + message); |
| } |
| |
| /** <execute-query> */ |
| protected void startExecuteQueryElement() { |
| switch (state) { |
| case SQLTransformer.STATE_OUTSIDE: |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| // Create root query (if query == null), or child query |
| this.query = new Query(this.query); |
| this.query.enableLogging(getLogger().getChildLogger("query")); |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start execute query element"); |
| } |
| } |
| |
| /** <*> */ |
| protected void startValueElement(String name) |
| throws SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| this.stack.push(name); |
| startTextRecording(); |
| state = SQLTransformer.STATE_INSIDE_VALUE_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start value element: " + name); |
| } |
| } |
| |
| /** <query> */ |
| protected void startQueryElement(Attributes attributes) |
| throws SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| startTextRecording(); |
| state = SQLTransformer.STATE_INSIDE_QUERY_ELEMENT; |
| |
| String isUpdate = attributes.getValue("", SQLTransformer.MAGIC_UPDATE_ATTRIBUTE); |
| if (isUpdate != null && !isUpdate.equalsIgnoreCase("false")) { |
| query.setUpdate(true); |
| } |
| |
| String isProcedure = attributes.getValue("", SQLTransformer.MAGIC_STORED_PROCEDURE_ATTRIBUTE); |
| if (isProcedure != null && !isProcedure.equalsIgnoreCase("false")) { |
| query.setStoredProcedure(true); |
| } |
| |
| String name = attributes.getValue("", SQLTransformer.MAGIC_NAME_ATTRIBUTE); |
| if (name != null) { |
| query.setName(name); |
| } |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start query element"); |
| } |
| } |
| |
| /** </query> */ |
| protected void endQueryElement() |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_QUERY_ELEMENT: |
| final String value = endTextRecording(); |
| if (value.length() > 0) { |
| query.addQueryPart(value); |
| } |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a stop query element"); |
| } |
| } |
| |
| /** </*> */ |
| protected void endValueElement() |
| throws SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_VALUE_ELEMENT: |
| final String name = (String) this.stack.pop(); |
| final String value = endTextRecording(); |
| query.setParameter(name, value); |
| this.state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting an end value element"); |
| } |
| } |
| |
| /** </execute-query> */ |
| protected void endExecuteQueryElement() throws SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| if (query.parent == null) { |
| query.executeQuery(); |
| query = null; |
| state = SQLTransformer.STATE_OUTSIDE; |
| } else { |
| query.parent.addNestedQuery(query); |
| query = query.parent; |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| } |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting an end execute query element"); |
| } |
| } |
| |
| /** <ancestor-value> */ |
| protected void startAncestorValueElement(Attributes attributes) |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_QUERY_ELEMENT: |
| int level = 0; |
| try { |
| level = Integer.parseInt(getAttributeValue(attributes, SQLTransformer.MAGIC_ANCESTOR_VALUE_LEVEL_ATTRIBUTE)); |
| } catch (Exception e) { |
| getLogger().debug("Invalid or missing value for " + SQLTransformer.MAGIC_ANCESTOR_VALUE_LEVEL_ATTRIBUTE + " attribute", e); |
| throwIllegalStateException("Ancestor value elements must have a " + |
| SQLTransformer.MAGIC_ANCESTOR_VALUE_LEVEL_ATTRIBUTE + " attribute"); |
| } |
| |
| String name = getAttributeValue(attributes, SQLTransformer.MAGIC_ANCESTOR_VALUE_NAME_ATTRIBUTE); |
| if (name == null) { |
| throwIllegalStateException("Ancestor value elements must have a " + |
| SQLTransformer.MAGIC_ANCESTOR_VALUE_NAME_ATTRIBUTE + " attribute"); |
| } |
| |
| final String value = endTextRecording(); |
| if (value.length() > 0) { |
| query.addQueryPart(value); |
| } |
| query.addQueryPart(new AncestorValue(level, name)); |
| startTextRecording(); |
| |
| state = SQLTransformer.STATE_INSIDE_ANCESTOR_VALUE_ELEMENT; |
| break; |
| default: |
| throwIllegalStateException("Not expecting a start ancestor value element"); |
| } |
| } |
| |
| /** </ancestor-value> */ |
| protected void endAncestorValueElement() { |
| state = SQLTransformer.STATE_INSIDE_QUERY_ELEMENT; |
| } |
| |
| /** <substitute-value> */ |
| protected void startSubstituteValueElement(Attributes attributes) |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_QUERY_ELEMENT: |
| String name = getAttributeValue(attributes, SQLTransformer.MAGIC_SUBSTITUTE_VALUE_NAME_ATTRIBUTE); |
| if (name == null) { |
| throwIllegalStateException("Substitute value elements must have a " + |
| SQLTransformer.MAGIC_SUBSTITUTE_VALUE_NAME_ATTRIBUTE + " attribute"); |
| } |
| String substitute = parameters.getParameter(name, null); |
| // Escape single quote |
| substitute = StringEscapeUtils.escapeSql(substitute); |
| |
| final String value = endTextRecording(); |
| if (value.length() > 0) { |
| query.addQueryPart(value); |
| } |
| query.addQueryPart(substitute); |
| startTextRecording(); |
| |
| state = SQLTransformer.STATE_INSIDE_SUBSTITUTE_VALUE_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start substitute value element"); |
| } |
| } |
| |
| /** </substitute-value> */ |
| protected void endSubstituteValueElement() { |
| state = SQLTransformer.STATE_INSIDE_QUERY_ELEMENT; |
| } |
| |
| /** <escape-string> */ |
| protected void startEscapeStringElement(Attributes attributes) |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_QUERY_ELEMENT: |
| final String value = endTextRecording(); |
| if (value.length() > 0) { |
| query.addQueryPart(value); |
| } |
| startTextRecording(); |
| |
| state = SQLTransformer.STATE_INSIDE_ESCAPE_STRING; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start escape-string element"); |
| } |
| } |
| |
| /** </escape-string> */ |
| protected void endEscapeStringElement() |
| throws SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_ESCAPE_STRING: |
| String value = endTextRecording(); |
| if (value.length() > 0) { |
| value = StringEscapeUtils.escapeSql(value); |
| value = StringUtils.replace(value, "\\", "\\\\"); |
| query.addQueryPart(value); |
| } |
| startTextRecording(); |
| state = SQLTransformer.STATE_INSIDE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a end escape-string element"); |
| } |
| } |
| |
| /** <xml> */ |
| protected void startXmlElement(Attributes attributes) |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_QUERY_ELEMENT: |
| final String value = endTextRecording(); |
| if (value.length() > 0) { |
| query.addQueryPart(value); |
| } |
| startSerializedXMLRecording( null); |
| |
| state = SQLTransformer.STATE_INSIDE_XML; |
| //this.getLogger().debug("startXmlElement: "); |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start escape-string element"); |
| } |
| } |
| |
| /** </xml> */ |
| protected void endXmlElement() |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_XML: |
| String value = endSerializedXMLRecording(); |
| //this.getLogger().debug("endXmlElement: " + value); |
| if (value.length() > 0) { |
| value = StringEscapeUtils.escapeSql(value); |
| query.addQueryPart(value); |
| } |
| startTextRecording(); |
| state = SQLTransformer.STATE_INSIDE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a end escape-string element"); |
| } |
| } |
| |
| /** <xml> */ |
| protected void startInXmlParameterElement(Attributes attributes) |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| String nr = getAttributeValue(attributes, SQLTransformer.MAGIC_IN_PARAMETER_NR_ATTRIBUTE); |
| this.stack.push(nr); |
| startSerializedXMLRecording( null); |
| |
| state = SQLTransformer.STATE_INSIDE_IN_XML_PARAMETER_ELEMENT; |
| //this.getLogger().debug("startXmlElement: "); |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a start escape-string element"); |
| } |
| } |
| |
| /** </xml> */ |
| protected void endInXmlParameterElement() |
| throws ProcessingException, SAXException { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_IN_XML_PARAMETER_ELEMENT: |
| String value = endSerializedXMLRecording(); |
| //this.getLogger().debug("endXmlElement: "+value); |
| if (value.length() > 0) { |
| int position = Integer.parseInt((String)this.stack.pop()); |
| query.setInXmlParameter(position, value); |
| } |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting a end escape-string element"); |
| } |
| } |
| /** <in-parameter> */ |
| protected void startInParameterElement(Attributes attributes) { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| String nr = getAttributeValue(attributes, SQLTransformer.MAGIC_IN_PARAMETER_NR_ATTRIBUTE); |
| String value = getAttributeValue(attributes, SQLTransformer.MAGIC_IN_PARAMETER_VALUE_ATTRIBUTE); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("IN PARAMETER NR " + nr + "; VALUE " + value); |
| } |
| |
| int position = Integer.parseInt(nr); |
| query.setInParameter(position, value); |
| state = SQLTransformer.STATE_INSIDE_IN_PARAMETER_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting an in-parameter element"); |
| } |
| } |
| |
| /** </in-parameter> */ |
| protected void endInParameterElement() { |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| } |
| |
| /** <out-parameter> */ |
| protected void startOutParameterElement(Attributes attributes) { |
| switch (state) { |
| case SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT: |
| String name = getAttributeValue(attributes, SQLTransformer.MAGIC_OUT_PARAMETER_NAME_ATTRIBUTE); |
| String nr = getAttributeValue(attributes, SQLTransformer.MAGIC_OUT_PARAMETER_NR_ATTRIBUTE); |
| String type = getAttributeValue(attributes, SQLTransformer.MAGIC_OUT_PARAMETER_TYPE_ATTRIBUTE); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("OUT PARAMETER NAME" + name + ";NR " + nr + "; TYPE " + type); |
| } |
| |
| int position = Integer.parseInt(nr); |
| query.setOutParameter(position, type, name); |
| state = SQLTransformer.STATE_INSIDE_OUT_PARAMETER_ELEMENT; |
| break; |
| |
| default: |
| throwIllegalStateException("Not expecting an out-parameter element"); |
| } |
| } |
| |
| /** </out-parameter> */ |
| protected void endOutParameterElement() { |
| state = SQLTransformer.STATE_INSIDE_EXECUTE_QUERY_ELEMENT; |
| } |
| |
| /** |
| * ContentHandler method |
| */ |
| public void startTransformingElement(String uri, String name, String raw, Attributes attributes) |
| throws ProcessingException, SAXException { |
| if (name.equals(SQLTransformer.MAGIC_EXECUTE_QUERY)) { |
| startExecuteQueryElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_QUERY)) { |
| startQueryElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_ANCESTOR_VALUE)) { |
| startAncestorValueElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_SUBSTITUTE_VALUE)) { |
| startSubstituteValueElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_IN_PARAMETER)) { |
| startInParameterElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_OUT_PARAMETER)) { |
| startOutParameterElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_ESCAPE_STRING)) { |
| startEscapeStringElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_XML)) { |
| startXmlElement(attributes); |
| } else if (name.equals(SQLTransformer.MAGIC_IN_XML_PARAMETER)) { |
| startInXmlParameterElement(attributes); |
| } else { |
| startValueElement(name); |
| } |
| } |
| |
| /** |
| * ContentHandler method |
| */ |
| public void endTransformingElement(String uri, String name, String raw) |
| throws ProcessingException, IOException, SAXException { |
| if (name.equals(SQLTransformer.MAGIC_EXECUTE_QUERY)) { |
| endExecuteQueryElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_QUERY)) { |
| endQueryElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_ANCESTOR_VALUE)) { |
| endAncestorValueElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_SUBSTITUTE_VALUE)) { |
| endSubstituteValueElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_IN_PARAMETER)) { |
| endInParameterElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_OUT_PARAMETER)) { |
| endOutParameterElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_ESCAPE_STRING)) { |
| endEscapeStringElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_XML)) { |
| endXmlElement(); |
| } else if (name.equals(SQLTransformer.MAGIC_IN_XML_PARAMETER)) { |
| endInXmlParameterElement(); |
| } else { |
| endValueElement(); |
| } |
| } |
| |
| // |
| // Helper methods for the Query |
| // |
| |
| /** |
| * Qualifies an element name by giving it a prefix. |
| * @param name the element name |
| * @param prefix the prefix to qualify with |
| * @return a namespace qualified name that is correct |
| */ |
| protected String nsQualify(String name, String prefix) { |
| if (StringUtils.isEmpty(name)) { |
| return name; |
| } |
| |
| if (StringUtils.isNotEmpty(prefix)) { |
| return prefix + ":" + name; |
| } |
| |
| return name; |
| } |
| |
| /** |
| * Helper method for generating SAX events |
| */ |
| protected void start(String uri, String prefix, String name, Attributes attr) |
| throws SAXException { |
| try { |
| super.startTransformingElement(uri, name, nsQualify(name, prefix), attr); |
| } catch (IOException e) { |
| throw new SAXException(e); |
| } catch (ProcessingException e) { |
| throw new SAXException(e); |
| } |
| } |
| |
| /** |
| * Helper method for generating SAX events |
| */ |
| protected void end(String uri, String prefix, String name) throws SAXException { |
| try { |
| super.endTransformingElement(uri, name, nsQualify(name, prefix)); |
| } catch (IOException e) { |
| throw new SAXException(e); |
| } catch (ProcessingException e) { |
| throw new SAXException(e); |
| } |
| } |
| |
| /** |
| * Helper method for generating SAX events |
| */ |
| protected void data(String data) throws SAXException { |
| if (data != null) { |
| super.characters(data.toCharArray(), 0, data.length()); |
| } |
| } |
| |
| /** |
| * Get 'name' for the connection which can be obtained using provided |
| * connection parameters. |
| */ |
| private String name(Parameters params) { |
| final boolean ownConnection = params.getParameterAsBoolean(SQLTransformer.MAGIC_OWN_CONNECTION, false); |
| if (ownConnection) { |
| return null; |
| } |
| |
| final String datasourceName = params.getParameter(SQLTransformer.MAGIC_CONNECTION, null); |
| if (datasourceName != null) { |
| return "ds:" + datasourceName; |
| } |
| |
| final String dburl = params.getParameter(SQLTransformer.MAGIC_DBURL, null); |
| if (dburl != null) { |
| final String username = params.getParameter(SQLTransformer.MAGIC_USERNAME, null); |
| final String password = params.getParameter(SQLTransformer.MAGIC_PASSWORD, null); |
| |
| if (username == null || password == null) { |
| return "db:@" + dburl; |
| } else { |
| return "db:" + username + ":" + password + "@" + dburl; |
| } |
| } |
| |
| // Nothing configured |
| return ""; |
| } |
| |
| /** |
| * Open database connection using provided parameters. |
| * Return null if neither datasource nor jndi URL configured. |
| */ |
| private Connection open(Parameters params) throws SQLException { |
| Connection result = null; |
| |
| // First check datasource name parameter |
| final String datasourceName = params.getParameter(SQLTransformer.MAGIC_CONNECTION, null); |
| if (datasourceName != null) { |
| // Use datasource components |
| if (this.datasources == null) { |
| throw new SQLException("Unable to get connection from datasource '" + datasourceName + "': " + |
| "No datasources configured in cocoon.xconf."); |
| } |
| |
| DataSourceComponent datasource = null; |
| try { |
| datasource = (DataSourceComponent) this.datasources.select(datasourceName); |
| for (int i = 0; i < this.connectAttempts && result == null; i++) { |
| try { |
| result = datasource.getConnection(); |
| } catch (SQLException e) { |
| if (i + 1 < this.connectAttempts) { |
| final long waittime = this.connectWaittime; |
| // Log exception if debug enabled. |
| if (getLogger().isDebugEnabled()) { |
| getLogger().info("Unable to get connection; waiting " + |
| waittime + "ms to try again.", e); |
| } else { |
| getLogger().info("Unable to get connection; waiting " + |
| waittime + "ms to try again."); |
| } |
| try { |
| Thread.sleep(waittime); |
| } catch (InterruptedException ex) { |
| /* ignored */ |
| } |
| } |
| } |
| } |
| } catch (ServiceException e) { |
| throw new SQLException("Unable to get connection from datasource '" + datasourceName + "': " + |
| "No such datasource."); |
| } finally { |
| if (datasource != null) { |
| this.datasources.release(datasource); |
| } |
| } |
| |
| if (result == null) { |
| throw new SQLException("Failed to obtain connection from datasource '" + datasourceName + "'. " + |
| "Made " + this.connectAttempts + " attempts with " |
| + this.connectWaittime + "ms interval"); |
| } |
| } else { |
| // Then, check connection URL parameter |
| final String dburl = params.getParameter(SQLTransformer.MAGIC_DBURL, null); |
| if (dburl != null) { |
| final String username = params.getParameter(SQLTransformer.MAGIC_USERNAME, null); |
| final String password = params.getParameter(SQLTransformer.MAGIC_PASSWORD, null); |
| final String prop = params.getParameter(SQLTransformer.MAGIC_PROP, null); |
| |
| if (username == null || password == null) { |
| result = DriverManager.getConnection(dburl); |
| } else if (prop == null) { |
| result = DriverManager.getConnection(dburl, username, password); |
| } else { |
| Properties props = new Properties(); |
| props.put("user", username ); |
| props.put("password", password); |
| int proppos = prop.indexOf('='); |
| if (proppos > 0) { |
| String propname = prop.substring(0, proppos); |
| String propvalue = prop.substring(proppos+1); |
| props.put(propname, propvalue); |
| } |
| result = DriverManager.getConnection(dburl, props); |
| } |
| } else { |
| // Nothing configured |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Attempt to parse string value |
| */ |
| private void stream(String value) throws ServiceException, SAXException, IOException { |
| try { |
| // Strip off the XML Declaration if there is one! |
| if (value.startsWith("<?xml ")) { |
| value = value.substring(value.indexOf("?>") + 2); |
| } |
| |
| // Lookup components |
| if (this.parser == null) { |
| this.parser = (SAXParser) manager.lookup(SAXParser.ROLE); |
| } |
| if (this.compiler == null) { |
| this.compiler = (XMLSerializer) manager.lookup(XMLSerializer.ROLE); |
| } |
| if (this.interpreter == null) { |
| this.interpreter = (XMLDeserializer) manager.lookup(XMLDeserializer.ROLE); |
| } |
| |
| this.parser.parse(new InputSource(new StringReader("<root>" + value + "</root>")), |
| this.compiler); |
| |
| IncludeXMLConsumer filter = new IncludeXMLConsumer(this, this); |
| filter.setIgnoreRootElement(true); |
| |
| this.interpreter.setConsumer(filter); |
| this.interpreter.deserialize(this.compiler.getSAXFragment()); |
| } finally { |
| // otherwise serializer won't be reset |
| if (this.compiler != null) { |
| manager.release(this.compiler); |
| this.compiler = null; |
| } |
| } |
| } |
| |
| /** |
| * One of the queries in the query tree formed from nested queries. |
| */ |
| private class Query extends AbstractLogEnabled { |
| |
| /** Parent query, or null for top level query */ |
| protected Query parent; |
| |
| /** Nested sub-queries we have. */ |
| protected final List nested = new ArrayList(); |
| |
| /** The parts of the query */ |
| protected final List parts = new ArrayList(); |
| |
| // |
| // Query Configuration |
| // |
| |
| /** Name of the query */ |
| protected String name; |
| |
| /** If this query is actually an update (insert, update, delete) */ |
| protected boolean isUpdate; |
| |
| /** If this query is actually a stored procedure */ |
| protected boolean isStoredProcedure; |
| |
| /** Query configuration parameters */ |
| protected Parameters params; |
| |
| /** The namespace uri of the XML output. Defaults to {@link SQLTransformer#namespaceURI}. */ |
| protected String outUri; |
| |
| /** The namespace prefix of the XML output. Defaults to 'sql'. */ |
| protected String outPrefix; |
| |
| /** rowset element name */ |
| protected String rowsetElement; |
| |
| /** row element name */ |
| protected String rowElement; |
| |
| /** number of rows attribute name */ |
| protected String nrOfRowsAttr = "nrofrows"; |
| |
| /** Query name attribute name */ |
| protected String nameAttr = "name"; |
| |
| /** Handling of case of column names in results */ |
| protected int columnCase; |
| |
| /** Registered IN parameters */ |
| protected Map inParameters; |
| |
| /** Registered IN XML parameters */ |
| protected Map inXmlParameters; |
| |
| /** Registered OUT parameters */ |
| protected Map outParameters; |
| |
| /** Mapping out parameters - objectModel */ |
| protected Map outParametersNames; |
| |
| /** Check if nr of rows need to be written out. */ |
| protected boolean showNrOfRows; |
| |
| /** Encoding we use for CLOB field */ |
| protected String clobEncoding; |
| |
| // |
| // Query State |
| // |
| |
| /** The connection */ |
| protected Connection conn; |
| |
| /** The 'name' of the connection */ |
| protected String connName; |
| |
| /** Is it our own connection? */ |
| protected boolean ownConn; |
| |
| /** Prepared statement */ |
| protected PreparedStatement pst; |
| |
| /** Callable statement */ |
| protected CallableStatement cst; |
| |
| /** The results, of course */ |
| protected ResultSet rs; |
| |
| /** And the results' metadata */ |
| protected ResultSetMetaData md; |
| |
| /** If it is an update/etc, the return value (num rows modified) */ |
| protected int rv = -1; |
| |
| |
| protected Query(Query parent) { |
| this.parent = parent; |
| this.params = new Parameters(); |
| this.params.merge(SQLTransformer.this.parameters); |
| } |
| |
| /** Add nested sub-query. */ |
| protected void addNestedQuery(Query query) { |
| nested.add(query); |
| } |
| |
| protected void addQueryPart(Object value) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Adding query part \"" + value + "\""); |
| } |
| parts.add(value); |
| } |
| |
| protected String getName() { |
| return name; |
| } |
| |
| protected void setName(String name) { |
| this.name = name; |
| } |
| |
| protected void setParameter(String name, String value) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Adding parameter name {" + name + "} value {" + value + "}"); |
| } |
| params.setParameter(name, value); |
| } |
| |
| protected void setUpdate(boolean flag) { |
| isUpdate = flag; |
| } |
| |
| protected void setStoredProcedure(boolean flag) { |
| isStoredProcedure = flag; |
| } |
| |
| protected void setInParameter(int pos, String val) { |
| if (inParameters == null) { |
| inParameters = new HashMap(); |
| } |
| inParameters.put(new Integer(pos), val); |
| } |
| |
| protected void setInXmlParameter(int pos, String val) { |
| if (inXmlParameters == null) { |
| inXmlParameters = new HashMap(); |
| } |
| inXmlParameters.put(new Integer(pos), val); |
| } |
| |
| protected void setOutParameter(int pos, String type, String name) { |
| if (outParameters == null) { |
| // make sure output parameters are ordered |
| outParameters = new TreeMap(); |
| outParametersNames = new HashMap(); |
| } |
| outParameters.put(new Integer(pos), type); |
| outParametersNames.put(new Integer(pos), name); |
| } |
| |
| private void setColumnCase(String columnCase) { |
| if (columnCase.equals("lowercase")) { |
| this.columnCase = -1; |
| } else if (columnCase.equals("uppercase")) { |
| this.columnCase = +1; |
| } else if (columnCase.equals("preserve")) { |
| // Do nothing |
| this.columnCase = 0; |
| } else { |
| getLogger().warn("[" + columnCase + "] is not a valid value for <column-case>. " + |
| "Column name retrieved from database will be used."); |
| } |
| } |
| |
| private void registerInParameters() throws SQLException { |
| if (inParameters != null) { |
| Iterator i = inParameters.keySet().iterator(); |
| while (i.hasNext()) { |
| Integer counter = (Integer) i.next(); |
| String value = (String) inParameters.get(counter); |
| try { |
| pst.setObject(counter.intValue(), value); |
| } catch (SQLException e) { |
| getLogger().error("Caught a SQLException", e); |
| throw e; |
| } |
| } |
| } |
| } |
| |
| private void registerInXmlParameters() throws SQLException { |
| if (inXmlParameters != null) { |
| Iterator i = inXmlParameters.keySet().iterator(); |
| while (i.hasNext()) { |
| Integer counter = (Integer) i.next(); |
| String value = (String) inXmlParameters.get(counter); |
| try { |
| pst.setString(counter.intValue(), value); |
| } catch (SQLException e) { |
| getLogger().error("Caught a SQLException", e); |
| throw e; |
| } |
| } |
| } |
| } |
| |
| private void registerOutParameters(CallableStatement cst) throws SQLException { |
| if (outParameters != null) { |
| Iterator i = outParameters.keySet().iterator(); |
| while (i.hasNext()) { |
| Integer counter = (Integer) i.next(); |
| String type = (String) outParameters.get(counter); |
| |
| int index = type.lastIndexOf("."); |
| String className, fieldName; |
| if (index == -1) { |
| getLogger().error("Invalid SQLType: " + type, null); |
| throw new SQLException("Invalid SQLType: " + type); |
| } |
| className = type.substring(0, index); |
| fieldName = type.substring(index + 1, type.length()); |
| |
| try { |
| Class clss = Class.forName(className); |
| Field fld = clss.getField(fieldName); |
| cst.registerOutParameter(counter.intValue(), fld.getInt(fieldName)); |
| } catch (Exception e) { |
| // Lots of different exceptions to catch |
| getLogger().error("Invalid SQLType: " + className + "." + fieldName, e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Open database connection |
| */ |
| private void open() throws SQLException { |
| this.connName = SQLTransformer.this.name(this.params); |
| |
| // Check first if connection sharing disabled |
| if (this.connName == null) { |
| this.conn = SQLTransformer.this.open(this.params); |
| this.ownConn = true; |
| return; |
| } |
| |
| // Iterate through parent queries and get appropriate connection |
| Query query = this.parent; |
| while (query != null) { |
| if (this.connName.equals(query.connName)) { |
| this.conn = query.conn; |
| this.ownConn = false; |
| return; |
| } |
| query = query.parent; |
| } |
| |
| // Check 'global' connection |
| if (this.connName.equals(SQLTransformer.this.connName)) { |
| // Use SQLTransformer configuration: it has same connection parameters |
| if (SQLTransformer.this.conn == null) { |
| SQLTransformer.this.conn = SQLTransformer.this.open(SQLTransformer.this.parameters); |
| } |
| |
| this.conn = SQLTransformer.this.conn; |
| this.ownConn = false; |
| return; |
| } |
| |
| // Create own connection |
| this.conn = SQLTransformer.this.open(this.params); |
| this.ownConn = true; |
| } |
| |
| /** |
| * This will be the meat of SQLTransformer, where the query is run. |
| */ |
| protected void executeQuery() throws SAXException { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Executing query " + this); |
| } |
| |
| this.outUri = this.params.getParameter(SQLTransformer.MAGIC_NS_URI_ELEMENT, SQLTransformer.this.namespaceURI); |
| this.outPrefix = this.params.getParameter(SQLTransformer.MAGIC_NS_PREFIX_ELEMENT, "sql"); |
| this.rowsetElement = this.params.getParameter(SQLTransformer.MAGIC_DOC_ELEMENT, "rowset"); |
| this.rowElement = this.params.getParameter(SQLTransformer.MAGIC_ROW_ELEMENT, "row"); |
| |
| this.showNrOfRows = parameters.getParameterAsBoolean(SQLTransformer.MAGIC_NR_OF_ROWS, false); |
| this.clobEncoding = parameters.getParameter(SQLTransformer.CLOB_ENCODING, ""); |
| if (this.clobEncoding.length() == 0) { |
| this.clobEncoding = null; |
| } |
| |
| // Start prefix mapping for output namespace, only if it's not mapped yet |
| final String prefix = SQLTransformer.this.findPrefixMapping(this.outUri); |
| if (prefix == null) { |
| SQLTransformer.this.startPrefixMapping(this.outPrefix, this.outUri); |
| } else { |
| this.outPrefix = prefix; |
| } |
| |
| boolean success = false; |
| try { |
| try { |
| open(); |
| execute(); |
| success = true; |
| } catch (SQLException e) { |
| getLogger().info("Failed to execute query " + this, e); |
| start(this.rowsetElement, EMPTY_ATTRIBUTES); |
| start(MAGIC_ERROR, EMPTY_ATTRIBUTES); |
| data(e.getMessage()); |
| end(MAGIC_ERROR); |
| end(this.rowsetElement); |
| } |
| |
| if (success) { |
| AttributesImpl attr = new AttributesImpl(); |
| if (showNrOfRows) { |
| attr.addAttribute("", this.nrOfRowsAttr, this.nrOfRowsAttr, "CDATA", String.valueOf(getNrOfRows())); |
| } |
| String name = getName(); |
| if (name != null) { |
| attr.addAttribute("", this.nameAttr, this.nameAttr, "CDATA", name); |
| } |
| start(this.rowsetElement, attr); |
| |
| // Serialize stored procedure output parameters |
| if (isStoredProcedure) { |
| serializeStoredProcedure(); |
| } |
| |
| // Serialize result set |
| while (next()) { |
| start(this.rowElement, EMPTY_ATTRIBUTES); |
| serializeRow(); |
| for (Iterator i = this.nested.iterator(); i.hasNext();) { |
| ((Query) i.next()).executeQuery(); |
| } |
| end(this.rowElement); |
| } |
| |
| end(this.rowsetElement); |
| } |
| } catch (SQLException e) { |
| getLogger().debug("Exception in executeQuery()", e); |
| throw new SAXException(e); |
| } finally { |
| close(); |
| } |
| |
| if (prefix == null) { |
| SQLTransformer.this.endPrefixMapping(this.outPrefix); |
| } |
| } |
| |
| /** |
| * Execute the query. Connection must be set already. |
| */ |
| private void execute() throws SQLException { |
| setColumnCase(params.getParameter(SQLTransformer.MAGIC_COLUMN_CASE, "lowercase")); |
| |
| // Construct query string |
| StringBuffer sb = new StringBuffer(); |
| for (Iterator i = parts.iterator(); i.hasNext();) { |
| Object object = i.next(); |
| if (object instanceof String) { |
| sb.append((String) object); |
| } else if (object instanceof AncestorValue) { |
| // Do a lookup into the ancestors' result's values |
| AncestorValue av = (AncestorValue) object; |
| Query query = this; |
| for (int k = av.level; k > 0; k--) { |
| query = query.parent; |
| } |
| sb.append(query.getColumnValue(av.name)); |
| } |
| } |
| |
| String query = StringUtils.replace(sb.toString().trim(), "\r", " ", -1); |
| // Test, if this is an update (by comparing with select) |
| if (!isStoredProcedure && !isUpdate) { |
| if (query.length() > 6 && !query.substring(0, 6).equalsIgnoreCase("SELECT")) { |
| isUpdate = true; |
| } |
| } |
| |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Executing " + query); |
| } |
| if (!isStoredProcedure) { |
| if (oldDriver) { |
| pst = conn.prepareStatement(query); |
| } else { |
| pst = conn.prepareStatement(query, |
| ResultSet.TYPE_SCROLL_INSENSITIVE, |
| ResultSet.CONCUR_READ_ONLY); |
| } |
| } else { |
| if (oldDriver) { |
| cst = conn.prepareCall(query); |
| } else { |
| cst = conn.prepareCall(query, |
| ResultSet.TYPE_SCROLL_INSENSITIVE, |
| ResultSet.CONCUR_READ_ONLY); |
| } |
| registerOutParameters(cst); |
| pst = cst; |
| } |
| |
| registerInParameters(); |
| registerInXmlParameters(); |
| boolean result = pst.execute(); |
| if (result) { |
| rs = pst.getResultSet(); |
| md = rs.getMetaData(); |
| } else { |
| rv = pst.getUpdateCount(); |
| } |
| } |
| |
| protected int getNrOfRows() throws SQLException { |
| int nr = 0; |
| |
| if (rs != null) { |
| if (oldDriver) { |
| nr = -1; |
| } else { |
| try { |
| rs.last(); |
| nr = rs.getRow(); |
| rs.beforeFirst(); |
| } catch (NullPointerException e) { |
| // A NullPointerException here crashes a whole lot of C2 -- |
| // catching it so it won't do any harm for now, but seems like it should be solved seriously |
| getLogger().error("NPE while getting the nr of rows", e); |
| } |
| } |
| } else { |
| if (outParameters != null) { |
| nr = outParameters.size(); |
| } |
| } |
| return nr; |
| } |
| |
| protected String getColumnValue(ResultSet rs, int i) throws SQLException { |
| final int type = rs.getMetaData().getColumnType(i); |
| if (type == java.sql.Types.DOUBLE) { |
| return getStringValue(rs.getBigDecimal(i)); |
| } else if (type == java.sql.Types.CLOB) { |
| return getStringValue(rs.getClob(i)); |
| } else { |
| return getStringValue(rs.getObject(i)); |
| } |
| } |
| |
| // fix not applied here because there is no metadata from Name -> number and coltype |
| // for a given "name" versus number. That being said this shouldn't be an issue |
| // as this function is only called for ancestor lookups. |
| protected String getColumnValue(String name) throws SQLException { |
| //noinspection UnnecessaryLocalVariable |
| String retval = getStringValue(rs.getObject(name)); |
| // if (rs.getMetaData().getColumnType( name ) == java.sql.Types.DOUBLE) |
| // retval = transformer.getStringValue( rs.getBigDecimal( name ) ); |
| return retval; |
| } |
| |
| protected boolean next() throws SQLException { |
| // If rv is not -1, then an SQL insert, update, etc, has |
| // happened (see JDBC docs - return codes for executeUpdate) |
| if (rv != -1) { |
| // Output row with return code. Once. |
| return true; |
| } |
| |
| if (rs != null && rs.next()) { |
| // Have next row |
| return true; |
| } |
| |
| while (pst.getMoreResults()) { |
| rs = pst.getResultSet(); |
| md = rs.getMetaData(); |
| if (rs.next()) { |
| // Have next row in next result set |
| return true; |
| } |
| } |
| |
| // Nothing left |
| return false; |
| } |
| |
| /** |
| * Closes all the resources, ignores (but logs) exceptions. |
| */ |
| protected void close() { |
| if (rs != null) { |
| try { |
| rs.close(); |
| } catch (SQLException e) { |
| getLogger().info("Unable to close the result set.", e); |
| } |
| // This prevents us from using the resultset again. |
| rs = null; |
| } |
| |
| if (pst != null && pst != cst) { |
| try { |
| pst.close(); |
| } catch (SQLException e) { |
| getLogger().info("Unable to close the statement.", e); |
| } |
| } |
| // Prevent using pst again. |
| pst = null; |
| |
| if (cst != null) { |
| try { |
| cst.close(); |
| } catch (SQLException e) { |
| getLogger().info("Unable to close the statement.", e); |
| } |
| // Prevent using cst again. |
| cst = null; |
| } |
| |
| try { |
| if (ownConn && conn != null) { |
| conn.close(); |
| } |
| } catch (SQLException e) { |
| getLogger().info("Unable to close the connection", e); |
| } |
| // Prevent using conn again. |
| conn = null; |
| } |
| |
| protected void serializeData(String value) |
| throws SQLException, SAXException { |
| if (value != null) { |
| value = value.trim(); |
| // Could this be XML ? |
| if (value.length() > 0 && value.charAt(0) == '<') { |
| try { |
| stream(value); |
| } catch (Exception ignored) { |
| // FIXME: bad coding "catch(Exception)" |
| // If an exception occured the data was not (valid) xml |
| data(value); |
| } |
| } else { |
| data(value); |
| } |
| } |
| } |
| |
| protected void serializeRow() |
| throws SQLException, SAXException { |
| if (rv != -1) { |
| start("returncode", EMPTY_ATTRIBUTES); |
| serializeData(String.valueOf(rv)); |
| end("returncode"); |
| // We only want the return code shown once. |
| // Reset rv so next() returns false next time. |
| rv = -1; |
| } else { |
| for (int i = 1; i <= md.getColumnCount(); i++) { |
| String columnName = getColumnName(md.getColumnName(i)); |
| start(columnName, EMPTY_ATTRIBUTES); |
| serializeData(getColumnValue(rs, i)); |
| end(columnName); |
| } |
| } |
| } |
| |
| private void serializeResultSet(ResultSet rs) throws SQLException, SAXException { |
| final ResultSetMetaData md = rs.getMetaData(); |
| final int n = md.getColumnCount(); |
| |
| // Get column names |
| final String[] columns = new String[n + 1]; |
| for (int i = 1; i <= n; i++) { |
| columns[i] = getColumnName(md.getColumnName(i)); |
| } |
| |
| // Process rows |
| while (rs.next()) { |
| start(rowElement, EMPTY_ATTRIBUTES); |
| for (int i = 1; i <= n; i++) { |
| start(columns[i], EMPTY_ATTRIBUTES); |
| serializeData(getColumnValue(rs, i)); |
| end(columns[i]); |
| } |
| end(this.rowElement); |
| } |
| } |
| |
| protected void serializeStoredProcedure() |
| throws SQLException, SAXException { |
| if (outParametersNames == null || cst == null) { |
| return; |
| } |
| |
| Iterator itOutKeys = outParameters.keySet().iterator(); |
| while (itOutKeys.hasNext()) { |
| final Integer counter = (Integer) itOutKeys.next(); |
| try { |
| final Object obj = cst.getObject(counter.intValue()); |
| final String name = (String) outParametersNames.get(counter); |
| start(name, EMPTY_ATTRIBUTES); |
| |
| if (!(obj instanceof ResultSet)) { |
| serializeData(getStringValue(obj)); |
| } else { |
| final ResultSet rs = (ResultSet) obj; |
| try { |
| serializeResultSet(rs); |
| } finally { |
| try { |
| rs.close(); |
| } catch (SQLException e) { /* ignored */ } |
| } |
| } |
| |
| end(name); |
| } catch (SQLException e) { |
| getLogger().error("Caught a SQLException", e); |
| throw e; |
| } |
| } |
| } |
| |
| private String getColumnName(String columnName) { |
| switch (this.columnCase) { |
| case -1: |
| columnName = columnName.toLowerCase(); |
| break; |
| case +1: |
| columnName = columnName.toUpperCase(); |
| break; |
| default: |
| // Do nothing |
| } |
| return columnName; |
| } |
| |
| /** |
| * Convert object to string represenation |
| */ |
| private String getStringValue(Object object) throws SQLException { |
| if (object instanceof byte[]) { |
| // FIXME Encoding? |
| return new String((byte[]) object); |
| } |
| |
| if (object instanceof char[]) { |
| return new String((char[]) object); |
| } |
| |
| // Old behavior: Read bytes & decode |
| if (object instanceof Clob && this.clobEncoding != null) { |
| Clob clob = (Clob) object; |
| StringBuffer buffer = new StringBuffer(); |
| InputStream is = clob.getAsciiStream(); |
| try { |
| byte[] bytes = new byte[BUFFER_SIZE]; |
| int n; |
| while ((n = is.read(bytes)) > -1) { |
| buffer.append(new String(bytes, 0, n, this.clobEncoding)); |
| } |
| } catch (IOException e) { |
| throw new SQLException("Error reading stream from CLOB"); |
| } |
| return buffer.toString(); |
| } |
| |
| // Correct behavior: Read character data |
| if (object instanceof Clob) { |
| Clob clob = (Clob) object; |
| StringBuffer buffer = new StringBuffer(); |
| Reader cs = clob.getCharacterStream(); |
| try { |
| char[] chars = new char[BUFFER_SIZE]; |
| int n; |
| while ((n = cs.read(chars)) > -1) { |
| buffer.append(chars, 0, n); |
| } |
| } catch (IOException e) { |
| throw new SQLException("Error reading stream from CLOB"); |
| } |
| return buffer.toString(); |
| } |
| |
| if (object != null) { |
| return object.toString(); |
| } |
| |
| return ""; |
| } |
| |
| private void start(String name, Attributes attr) |
| throws SAXException { |
| SQLTransformer.this.start(this.outUri, this.outPrefix, name, attr); |
| } |
| |
| private void end(String name) throws SAXException { |
| SQLTransformer.this.end(this.outUri, this.outPrefix, name); |
| } |
| |
| private void data(String data) throws SAXException { |
| SQLTransformer.this.data(data); |
| } |
| } |
| |
| private static class AncestorValue { |
| protected int level; |
| protected String name; |
| |
| protected AncestorValue(int level, String name) { |
| this.level = level; |
| this.name = name; |
| } |
| |
| public String toString() { |
| return "<ancestor level " + level + ", name " + name + ">"; |
| } |
| } |
| |
| /** |
| * Stop recording of text and return the recorded information. |
| * @return The String, trimmed. |
| * |
| * NB. SQLTransformer needs to have a special version of this method |
| * It needs the TextRecorder to not trim whitespace from the queries it is building |
| * |
| */ |
| public String endTextRecording() |
| throws SAXException { |
| sendEndPrefixMapping(); |
| |
| TextRecorder recorder = (TextRecorder) removeRecorder(); |
| String text = recorder.getAllText(); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("End text recording. Text=" + text); |
| } |
| return text; |
| } |
| } |