/*
  Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FLOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program is free software; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; version 2
  of the License.

  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., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  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;

	private ExceptionInterceptor exceptionInterceptor;
	
	protected JDBC4MysqlSQLXML(ResultSetInternalMethods owner, int index, ExceptionInterceptor exceptionInterceptor) {
		this.owningResultSet = owner;
		this.columnIndexOfXml = index;
		this.fromResultSet = true;
		this.exceptionInterceptor = exceptionInterceptor;
	}

	protected JDBC4MysqlSQLXML(ExceptionInterceptor exceptionInterceptor) {
		this.fromResultSet = false;
		this.exceptionInterceptor = exceptionInterceptor;
	}

	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", this.exceptionInterceptor);
		}
	}

	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, this.exceptionInterceptor);
		}
	}

	/**
	 * 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, this.exceptionInterceptor);
				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, this.exceptionInterceptor);
				sqlEx.initCause(ex);
				
				throw sqlEx;
			}
		} else {
			throw SQLError.createSQLException("XML Source of type \""
					+ clazz.toString() + "\" Not supported.",
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
		}
	}

	/**
	 * 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, this.exceptionInterceptor);
				sqlEx.initCause(ex);
				
				throw sqlEx;
			}
		} else {
			throw SQLError.createSQLException("XML Result of type \""
					+ clazz.toString() + "\" Not supported.",
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
		}
	}

	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, this.exceptionInterceptor);
			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, this.exceptionInterceptor);
			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("&lt;");
				break;
			
			case '>': 
				this.buf.append("&gt;");
				break;
			
			case '&':
				this.buf.append("&amp;");
				break;
			
			case '"': 

				if (!isAttributeData) {
					this.buf.append("\"");
				}
				else {
					this.buf.append("&quot;");
				}

				break;
		
			case '\r': 
				this.buf.append("&#xD;");
				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);
				}        		
			}
		}
	}
}