/* Copyright 2002-2007 MySQL AB, 2008 Sun Microsystems This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation. There are special exceptions to the terms and conditions of the GPL as it is applied to this software. View the full text of the exception in file EXCEPTIONS-CONNECTOR-J in the directory of this software distribution. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.mysql.jdbc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stax.StAXResult; import javax.xml.transform.stax.StAXSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.SAXException; public class JDBC4MysqlSQLXML implements SQLXML { private XMLInputFactory inputFactory; private XMLOutputFactory outputFactory; private String stringRep; private ResultSetInternalMethods owningResultSet; private int columnIndexOfXml; private boolean fromResultSet; private boolean isClosed = false; private boolean workingWithResult; private DOMResult asDOMResult; private SAXResult asSAXResult; private SimpleSaxToReader saxToReaderConverter; private StringWriter asStringWriter; private ByteArrayOutputStream asByteArrayOutputStream; protected JDBC4MysqlSQLXML(ResultSetInternalMethods owner, int index) { this.owningResultSet = owner; this.columnIndexOfXml = index; this.fromResultSet = true; } protected JDBC4MysqlSQLXML() { this.fromResultSet = false; } public synchronized void free() throws SQLException { this.stringRep = null; this.asDOMResult = null; this.asSAXResult = null; this.inputFactory = null; this.outputFactory = null; this.owningResultSet = null; this.workingWithResult = false; this.isClosed = true; } public synchronized String getString() throws SQLException { checkClosed(); checkWorkingWithResult(); if (this.fromResultSet) { return this.owningResultSet.getString(this.columnIndexOfXml); } return this.stringRep; } private synchronized void checkClosed() throws SQLException { if (this.isClosed) { throw SQLError .createSQLException("SQLXMLInstance has been free()d"); } } private synchronized void checkWorkingWithResult() throws SQLException { if (this.workingWithResult) { throw SQLError .createSQLException( "Can't perform requested operation after getResult() has been called to write XML data", SQLError.SQL_STATE_ILLEGAL_ARGUMENT); } } /** * Sets the XML value designated by this SQLXML instance to the given String * representation. The format of this String is defined by * org.xml.sax.InputSource, where the characters in the stream represent the * unicode code points for XML according to section 2 and appendix B of the * XML 1.0 specification. Although an encoding declaration other than * unicode may be present, the encoding of the String is unicode. The * behavior of this method is the same as ResultSet.updateString() when the * designated column of the ResultSet has a type java.sql.Types of SQLXML. * <p> * The SQL XML object becomes not writeable when this method is called and * may also become not readable depending on implementation. * * @param value * the XML value * @throws SQLException * if there is an error processing the XML value. The getCause() * method of the exception may provide a more detailed * exception, for example, if the stream does not contain valid * characters. An exception is thrown if the state is not * writable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized void setString(String str) throws SQLException { checkClosed(); checkWorkingWithResult(); this.stringRep = str; this.fromResultSet = false; } public synchronized boolean isEmpty() throws SQLException { checkClosed(); checkWorkingWithResult(); if (!this.fromResultSet) { return this.stringRep == null || this.stringRep.length() == 0; } return false; } public synchronized InputStream getBinaryStream() throws SQLException { checkClosed(); checkWorkingWithResult(); return this.owningResultSet.getBinaryStream(this.columnIndexOfXml); } /** * Retrieves the XML value designated by this SQLXML instance as a * java.io.Reader object. The format of this stream is defined by * org.xml.sax.InputSource, where the characters in the stream represent the * unicode code points for XML according to section 2 and appendix B of the * XML 1.0 specification. Although an encoding declaration other than * unicode may be present, the encoding of the stream is unicode. The * behavior of this method is the same as ResultSet.getCharacterStream() * when the designated column of the ResultSet has a type java.sql.Types of * SQLXML. * <p> * The SQL XML object becomes not readable when this method is called and * may also become not writable depending on implementation. * * @return a stream containing the XML data. * @throws SQLException * if there is an error processing the XML value. The getCause() * method of the exception may provide a more detailed * exception, for example, if the stream does not contain valid * characters. An exception is thrown if the state is not * readable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized Reader getCharacterStream() throws SQLException { checkClosed(); checkWorkingWithResult(); return this.owningResultSet.getCharacterStream(this.columnIndexOfXml); } /** * Returns a Source for reading the XML value designated by this SQLXML * instance. Sources are used as inputs to XML parsers and XSLT * transformers. * <p> * Sources for XML parsers will have namespace processing on by default. The * systemID of the Source is implementation dependent. * <p> * The SQL XML object becomes not readable when this method is called and * may also become not writable depending on implementation. * <p> * Note that SAX is a callback architecture, so a returned SAXSource should * then be set with a content handler that will receive the SAX events from * parsing. The content handler will receive callbacks based on the contents * of the XML. * * <pre> * SAXSource saxSource = sqlxml.getSource(SAXSource.class); * XMLReader xmlReader = saxSource.getXMLReader(); * xmlReader.setContentHandler(myHandler); * xmlReader.parse(saxSource.getInputSource()); * </pre> * * @param sourceClass * The class of the source, or null. If the class is null, a * vendor specifc Source implementation will be returned. The * following classes are supported at a minimum: * * (MySQL returns a SAXSource if sourceClass == null) * * <pre> * javax.xml.transform.dom.DOMSource - returns a DOMSource * javax.xml.transform.sax.SAXSource - returns a SAXSource * javax.xml.transform.stax.StAXSource - returns a StAXSource * javax.xml.transform.stream.StreamSource - returns a StreamSource * </pre> * * @return a Source for reading the XML value. * @throws SQLException * if there is an error processing the XML value or if this * feature is not supported. The getCause() method of the * exception may provide a more detailed exception, for example, * if an XML parser exception occurs. An exception is thrown if * the state is not readable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized Source getSource(Class clazz) throws SQLException { checkClosed(); checkWorkingWithResult(); // Note that we try and use streams here wherever possible // for the day that the server actually supports streaming // from server -> client (futureproofing) if (clazz == null || clazz.equals(SAXSource.class)) { InputSource inputSource = null; if (this.fromResultSet) { inputSource = new InputSource(this.owningResultSet .getCharacterStream(this.columnIndexOfXml)); } else { inputSource = new InputSource(new StringReader(this.stringRep)); } return new SAXSource(inputSource); } else if (clazz.equals(DOMSource.class)) { try { DocumentBuilderFactory builderFactory = DocumentBuilderFactory .newInstance(); builderFactory.setNamespaceAware(true); DocumentBuilder builder = builderFactory.newDocumentBuilder(); InputSource inputSource = null; if (this.fromResultSet) { inputSource = new InputSource(this.owningResultSet .getCharacterStream(this.columnIndexOfXml)); } else { inputSource = new InputSource(new StringReader( this.stringRep)); } return new DOMSource(builder.parse(inputSource)); } catch (Throwable t) { SQLException sqlEx = SQLError.createSQLException(t .getMessage(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT); sqlEx.initCause(t); throw sqlEx; } } else if (clazz.equals(StreamSource.class)) { Reader reader = null; if (this.fromResultSet) { reader = this.owningResultSet .getCharacterStream(this.columnIndexOfXml); } else { reader = new StringReader(this.stringRep); } return new StreamSource(reader); } else if (clazz.equals(StAXSource.class)) { try { Reader reader = null; if (this.fromResultSet) { reader = this.owningResultSet .getCharacterStream(this.columnIndexOfXml); } else { reader = new StringReader(this.stringRep); } return new StAXSource(this.inputFactory .createXMLStreamReader(reader)); } catch (XMLStreamException ex) { SQLException sqlEx = SQLError.createSQLException(ex .getMessage(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT); sqlEx.initCause(ex); throw sqlEx; } } else { throw SQLError.createSQLException("XML Source of type \"" + clazz.toString() + "\" Not supported.", SQLError.SQL_STATE_ILLEGAL_ARGUMENT); } } /** * Retrieves a stream that can be used to write the XML value that this * SQLXML instance represents. The stream begins at position 0. The bytes of * the stream are interpreted according to appendix F of the XML 1.0 * specification The behavior of this method is the same as * ResultSet.updateBinaryStream() when the designated column of the * ResultSet has a type java.sql.Types of SQLXML. * <p> * The SQL XML object becomes not writeable when this method is called and * may also become not readable depending on implementation. * * @return a stream to which data can be written. * @throws SQLException * if there is an error processing the XML value. An exception * is thrown if the state is not writable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized OutputStream setBinaryStream() throws SQLException { checkClosed(); checkWorkingWithResult(); this.workingWithResult = true; return setBinaryStreamInternal(); } private synchronized OutputStream setBinaryStreamInternal() throws SQLException { this.asByteArrayOutputStream = new ByteArrayOutputStream(); return this.asByteArrayOutputStream; } /** * Retrieves a stream to be used to write the XML value that this SQLXML * instance represents. The format of this stream is defined by * org.xml.sax.InputSource, where the characters in the stream represent the * unicode code points for XML according to section 2 and appendix B of the * XML 1.0 specification. Although an encoding declaration other than * unicode may be present, the encoding of the stream is unicode. The * behavior of this method is the same as ResultSet.updateCharacterStream() * when the designated column of the ResultSet has a type java.sql.Types of * SQLXML. * <p> * The SQL XML object becomes not writeable when this method is called and * may also become not readable depending on implementation. * * @return a stream to which data can be written. * @throws SQLException * if there is an error processing the XML value. The getCause() * method of the exception may provide a more detailed * exception, for example, if the stream does not contain valid * characters. An exception is thrown if the state is not * writable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized Writer setCharacterStream() throws SQLException { checkClosed(); checkWorkingWithResult(); this.workingWithResult = true; return setCharacterStreamInternal(); } private synchronized Writer setCharacterStreamInternal() throws SQLException { this.asStringWriter = new StringWriter(); return this.asStringWriter; } /** * Returns a Result for setting the XML value designated by this SQLXML * instance. * <p> * The systemID of the Result is implementation dependent. * <p> * The SQL XML object becomes not writeable when this method is called and * may also become not readable depending on implementation. * <p> * Note that SAX is a callback architecture and the returned SAXResult has a * content handler assigned that will receive the SAX events based on the * contents of the XML. Call the content handler with the contents of the * XML document to assign the values. * * <pre> * SAXResult saxResult = sqlxml.setResult(SAXResult.class); * ContentHandler contentHandler = saxResult.getXMLReader().getContentHandler(); * contentHandler.startDocument(); * // set the XML elements and attributes into the result * contentHandler.endDocument(); * </pre> * * @param resultClass * The class of the result, or null. If resultClass is null, a * vendor specific Result implementation will be returned. The * following classes are supported at a minimum: * * <pre> * javax.xml.transform.dom.DOMResult - returns a DOMResult * javax.xml.transform.sax.SAXResult - returns a SAXResult * javax.xml.transform.stax.StAXResult - returns a StAXResult * javax.xml.transform.stream.StreamResult - returns a StreamResult * </pre> * * @return Returns a Result for setting the XML value. * @throws SQLException * if there is an error processing the XML value or if this * feature is not supported. The getCause() method of the * exception may provide a more detailed exception, for example, * if an XML parser exception occurs. An exception is thrown if * the state is not writable. * @exception SQLFeatureNotSupportedException * if the JDBC driver does not support this method * @since 1.6 */ public synchronized Result setResult(Class clazz) throws SQLException { checkClosed(); checkWorkingWithResult(); this.workingWithResult = true; this.asDOMResult = null; this.asSAXResult = null; this.saxToReaderConverter = null; this.stringRep = null; this.asStringWriter = null; this.asByteArrayOutputStream = null; if (clazz == null || clazz.equals(SAXResult.class)) { this.saxToReaderConverter = new SimpleSaxToReader(); this.asSAXResult = new SAXResult(this.saxToReaderConverter); return this.asSAXResult; } else if (clazz.equals(DOMResult.class)) { this.asDOMResult = new DOMResult(); return this.asDOMResult; } else if (clazz.equals(StreamResult.class)) { return new StreamResult(setCharacterStreamInternal()); } else if (clazz.equals(StAXResult.class)) { try { if (this.outputFactory == null) { this.outputFactory = XMLOutputFactory.newInstance(); } return new StAXResult(this.outputFactory .createXMLEventWriter(setCharacterStreamInternal())); } catch (XMLStreamException ex) { SQLException sqlEx = SQLError.createSQLException(ex .getMessage(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT); sqlEx.initCause(ex); throw sqlEx; } } else { throw SQLError.createSQLException("XML Result of type \"" + clazz.toString() + "\" Not supported.", SQLError.SQL_STATE_ILLEGAL_ARGUMENT); } } private Reader binaryInputStreamStreamToReader(ByteArrayOutputStream out) { try { // There's got to be an easier way to do this, but // I don't feel like coding up Appendix F of the XML Spec // myself, when there's a reusable way to do it, and we // can warn folks away from BINARY xml streams that have // to be parsed to determine the character encoding :P String encoding = "UTF-8"; try { ByteArrayInputStream bIn = new ByteArrayInputStream(out .toByteArray()); XMLStreamReader reader = this.inputFactory .createXMLStreamReader(bIn); int eventType = 0; while ((eventType = reader.next()) != XMLStreamReader.END_DOCUMENT) { if (eventType == XMLStreamReader.START_DOCUMENT) { String possibleEncoding = reader.getEncoding(); if (possibleEncoding != null) { encoding = possibleEncoding; } break; } } } catch (Throwable t) { // ignore, dealt with later when the string can't be parsed // into valid XML } return new StringReader(new String(out.toByteArray(), encoding)); } catch (UnsupportedEncodingException badEnc) { throw new RuntimeException(badEnc); } } protected String readerToString(Reader reader) throws SQLException { StringBuffer buf = new StringBuffer(); int charsRead = 0; char[] charBuf = new char[512]; try { while ((charsRead = reader.read(charBuf)) != -1) { buf.append(charBuf, 0, charsRead); } } catch (IOException ioEx) { SQLException sqlEx = SQLError.createSQLException(ioEx .getMessage(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT); sqlEx.initCause(ioEx); throw sqlEx; } return buf.toString(); } protected synchronized Reader serializeAsCharacterStream() throws SQLException { checkClosed(); if (this.workingWithResult) { // figure out what kind of result if (this.stringRep != null) { return new StringReader(this.stringRep); } if (this.asDOMResult != null) { return new StringReader(domSourceToString()); } if (this.asStringWriter != null) { // stax result return new StringReader(this.asStringWriter.toString()); } if (this.asSAXResult != null) { return this.saxToReaderConverter.toReader(); } if (this.asByteArrayOutputStream != null) { return binaryInputStreamStreamToReader(this.asByteArrayOutputStream); } } return this.owningResultSet.getCharacterStream(this.columnIndexOfXml); } protected String domSourceToString() throws SQLException { try { DOMSource source = new DOMSource(this.asDOMResult.getNode()); Transformer identity = TransformerFactory.newInstance() .newTransformer(); StringWriter stringOut = new StringWriter(); Result result = new StreamResult(stringOut); identity.transform(source, result); return stringOut.toString(); } catch (Throwable t) { SQLException sqlEx = SQLError.createSQLException(t .getMessage(), SQLError.SQL_STATE_ILLEGAL_ARGUMENT); sqlEx.initCause(t); throw sqlEx; } } protected synchronized String serializeAsString() throws SQLException { checkClosed(); if (this.workingWithResult) { // figure out what kind of result if (this.stringRep != null) { return this.stringRep; } if (this.asDOMResult != null) { return domSourceToString(); } if (this.asStringWriter != null) { // stax result return this.asStringWriter.toString(); } if (this.asSAXResult != null) { return readerToString(this.saxToReaderConverter.toReader()); } if (this.asByteArrayOutputStream != null) { return readerToString( binaryInputStreamStreamToReader(this.asByteArrayOutputStream)); } } return this.owningResultSet.getString(this.columnIndexOfXml); } /* * The SimpleSaxToReader class is an adaptation of the SAX "Writer" * example from the Apache XercesJ-2 Project. The license for this * code is as follows: * * 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. */ class SimpleSaxToReader extends DefaultHandler { StringBuffer buf = new StringBuffer(); public void startDocument() throws SAXException { buf.append("<?xml version='1.0' encoding='UTF-8'?>"); } public void endDocument() throws SAXException { // Do we need to override this? } public void startElement(String namespaceURI, String sName, String qName, Attributes attrs) throws SAXException { this.buf.append("<"); this.buf.append(qName); if (attrs != null) { for (int i = 0; i < attrs.getLength(); i++) { this.buf.append(" "); this.buf.append(attrs.getQName(i)).append("=\""); escapeCharsForXml(attrs.getValue(i), true); this.buf.append("\""); } } this.buf.append(">"); } public void characters(char buf[], int offset, int len) throws SAXException { if (!this.inCDATA) { escapeCharsForXml(buf, offset, len, false); } else { this.buf.append(buf, offset, len); } } public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { characters(ch, start, length); } private boolean inCDATA = false; public void startCDATA() throws SAXException { this.buf.append("<![CDATA["); this.inCDATA = true; } public void endCDATA() throws SAXException { this.inCDATA = false; this.buf.append("]]>"); } public void comment(char ch[], int start, int length) throws SAXException { // if (!fCanonical && fElementDepth > 0) { this.buf.append("<!--"); for (int i = 0; i < length; ++i) { this.buf.append(ch[start + i]); } this.buf.append("-->"); // } } Reader toReader() { return new StringReader(this.buf.toString()); } private void escapeCharsForXml(String str, boolean isAttributeData) { if (str == null) { return; } int strLen = str.length(); for (int i = 0; i < strLen; i++) { escapeCharsForXml(str.charAt(i), isAttributeData); } } private void escapeCharsForXml(char[] buf, int offset, int len, boolean isAttributeData) { if (buf == null) { return; } for (int i = 0; i < len; i++) { escapeCharsForXml(buf[offset + i], isAttributeData); } } private void escapeCharsForXml(char c, boolean isAttributeData) { switch (c) { case '<': this.buf.append("<"); break; case '>': this.buf.append(">"); break; case '&': this.buf.append("&"); break; case '"': if (!isAttributeData) { this.buf.append("\""); } else { this.buf.append("""); } break; case '\r': this.buf.append(" "); break; default: if (((c >= 0x01 && c <= 0x1F && c != 0x09 && c != 0x0A) || (c >= 0x7F && c <= 0x9F) || c == 0x2028) || isAttributeData && (c == 0x09 || c == 0x0A)) { this.buf.append("&#x"); this.buf.append(Integer.toHexString(c).toUpperCase()); this.buf.append(";"); } else { this.buf.append(c); } } } } }