/* * @(#)$Id$ * * The Apache Software License, Version 1.1 * * * Copyright (c) 2001 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Xalan" and "Apache Software Foundation" must * not be used to endorse or promote products derived from this * software without prior written permission. For written * permission, please contact apache@apache.org. * * 5. Products derived from this software may not be called "Apache", * nor may "Apache" appear in their name, without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation and was * originally based on software copyright (c) 2001, Sun * Microsystems., http://www.sun.com. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * * @author Jacek Ambroziak * @author Santiago Pericas-Geertsen * @author Morten Jorgensen * */ package org.apache.xalan.xsltc.runtime; import java.io.*; import java.util.Stack; import org.apache.xalan.xsltc.*; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; public final class TextOutput implements TransletOutputHandler { public static final int UNKNOWN = -1; public static final int TEXT = 0; public static final int XML = 1; public static final int HTML = 2; public static final int QNAME = 3; private int _outputType; private boolean _escapeChars = false; private boolean _startTagOpen = false; private boolean _cdataTagOpen = false; private boolean _headTagOpen = false; // Contains commonly used attributes (for speeding up output) private Hashtable _attributeTemplates = new Hashtable(); // Contains all elements that should be output as CDATA sections private Hashtable _cdataElements = new Hashtable(); private static final String XML_PREFIX = "xml"; private static final char[] AMP = "&".toCharArray(); private static final char[] LT = "<".toCharArray(); private static final char[] GT = ">".toCharArray(); private static final char[] CRLF = " ".toCharArray(); private static final char[] QUOTE = """.toCharArray(); private static final char[] NBSP = " ".toCharArray(); private static final int AMP_length = AMP.length; private static final int LT_length = LT.length; private static final int GT_length = GT.length; private static final int CRLF_length = CRLF.length; private static final int QUOTE_length = QUOTE.length; private static final int NBSP_length = NBSP.length; private static final char[] BEGCDATA = "<![CDATA[".toCharArray(); private static final char[] ENDCDATA = "]]>".toCharArray(); private static final char[] CNTCDATA = "]]]]><![CDATA[>".toCharArray(); private static final char[] BEGCOMM = "<!--".toCharArray(); private static final char[] ENDCOMM = "-->".toCharArray(); private static final String EMPTYSTRING = ""; private AttributeList _attributes = new AttributeList(); private String _elementName = null; private String _header; private Hashtable _namespaces; private Stack _nodeStack; private Stack _prefixStack; private Stack _qnameStack; // Holds the current tree depth (see startElement() and endElement()). private int _depth = 0; private String _encoding; private ContentHandler _saxHandler; /** * Constructor */ public TextOutput(ContentHandler handler) throws IOException { _saxHandler = handler; init(); } /** * Constructor */ public TextOutput(ContentHandler handler, String encoding) throws IOException { _saxHandler = handler; init(); _encoding = encoding; } /** * Initialise global variables */ private void init() throws IOException { _escapeChars = false; _startTagOpen = false; _cdataTagOpen = false; _outputType = UNKNOWN; _header = null; _encoding = "utf-8"; _qnameStack = new Stack(); // Empty all our hashtables _attributeTemplates.clear(); _cdataElements.clear(); initNamespaces(); } /** * Set the output type. The type must be wither TEXT, XML or HTML. */ public void setType(int type) { try { _outputType = type; if (_saxHandler instanceof DefaultSAXOutputHandler) ((DefaultSAXOutputHandler)_saxHandler).setOutputType(type); } catch (SAXException e) { } } /** * Emit header through the SAX handler */ private void emitHeader() throws SAXException { // Make sure the _encoding string contains something if ((_encoding == null) || (_encoding == EMPTYSTRING)) _encoding = "utf-8"; // Output HTML header as META element if (_outputType == HTML) { AttributeList attrs = new AttributeList(); attrs.add("http-equiv","Content-Type"); attrs.add("content","text/html; charset="+_encoding); _saxHandler.startElement(null, null, "meta", attrs); _saxHandler.endElement(null, null, "meta"); } } /** * Turns output indentation on/off. Should only be set to on * if the output type is XML or HTML. */ public void setIndent(boolean indent) { if (_saxHandler instanceof DefaultSAXOutputHandler) { ((DefaultSAXOutputHandler)_saxHandler).setIndent(indent); } } /** * Directive to turn xml header declaration on/off. * bug fix # 1406. */ public void omitXmlDecl(boolean value) { if (_saxHandler instanceof DefaultSAXOutputHandler) { ((DefaultSAXOutputHandler)_saxHandler).omitXmlDecl(value); } } /** * This method is called when all the data needed for a call to the * SAX handler's startElement() method has been gathered. */ public void closeStartTag() throws TransletException { try { _startTagOpen = false; // Output current element, either as element or CDATA section if (_cdataElements.containsKey(_elementName)) { characters(BEGCDATA); _cdataTagOpen = true; } else { // Final check to assure that the element is within a namespace // that has been declared (all declarations for this element // should have been processed at this point). int col = _elementName.lastIndexOf(':'); if (col > 0) { final String prefix = _elementName.substring(0,col); final String localname = _elementName.substring(col+1); final String uri = lookupNamespace(prefix); if (uri == null) { throw new TransletException("Namespace for prefix "+ prefix+" has not been "+ "declared."); } _saxHandler.startElement(uri, localname, _elementName, _attributes); } else { final String uri = lookupNamespace(EMPTYSTRING); _saxHandler.startElement(uri, _elementName, _elementName, _attributes); } } // Insert <META> tag directly after <HEAD> element in HTML output if (_headTagOpen) { emitHeader(); _headTagOpen = false; } } catch (SAXException e) { throw new TransletException(e); } } /** * Turns special character escaping on/off. Note that characters will * never, even if this option is set to 'true', be escaped within * CDATA sections in output XML documents. */ public boolean setEscaping(boolean escape) throws TransletException { try { boolean oldSetting = _escapeChars; if (_outputType == UNKNOWN) { setType(XML); emitHeader(); oldSetting = true; } _escapeChars = escape; // bug # 1403, see also compiler/Text.java::translate method. if (_outputType == TEXT) { _escapeChars = false; } return(oldSetting); } catch (SAXException e) { throw(new TransletException(e)); } } /** * Output document stream flush */ public void flush() throws IOException { //_saxHandler.flush(); } /** * Output document stream close */ public void close() throws IOException { //_saxHandler.close(); } /** * The <xsl:output method="xml"/> instruction can specify that certain * XML elements should be output as CDATA sections. This methods allows * the translet to insert these elements into a hashtable of strings. * Every output element is looked up in this hashtable before it is * output. */ public void insertCdataElement(String elementName) { _cdataElements.put(elementName,EMPTYSTRING); } /** * Starts the output document. Outputs the document header if the * output type is set to XML. */ public void startDocument() throws TransletException { try { _saxHandler.startDocument(); if (_outputType == XML) { emitHeader(); _escapeChars = true; } } catch (SAXException e) { throw new TransletException(e); } } /** * Ends the document output. */ public void endDocument() throws TransletException { try { // Close any open start tag if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } // Set output type to XML (the default) if still unknown. if (_outputType == UNKNOWN) { setType(XML); emitHeader(); } // Close output document _saxHandler.endDocument(); } catch (SAXException e) { throw new TransletException(e); } } /** * Utility method - pass a string to the SAX handler's characters() method */ private void characters(String str) throws SAXException{ final char[] ch = str.toCharArray(); _saxHandler.characters(ch, 0, ch.length); } /** * Utility method - pass a whole character array to the SAX handler */ private void characters(char[] ch) throws SAXException{ _saxHandler.characters(ch, 0, ch.length); } /** * Send characters to the output document */ public void characters(char[] ch, int off, int len) throws TransletException { try { // Close any open start tag if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } // Set output type to XML (the default) if still unknown. if (_outputType == UNKNOWN) { setType(XML); emitHeader(); _escapeChars = true; } int limit = off + len; int offset = off; int i; // Take special precautions if within a CDATA section. If we // encounter the sequence ']]>' within the CDATA, we need to // break the section in two and leave the ']]' at the end of // the first CDATA and '>' at the beginning of the next. if (_cdataTagOpen && len>2) { for (i = off; i < limit-2; i++) { if (ch[i] == ']' && ch[i+1] == ']' && ch[i+2] == '>') { _saxHandler.characters(ch, offset, i - offset); characters(CNTCDATA); offset = i+3; i=i+2; // Skip next chars ']' and '>'. } } if (offset < limit) { _saxHandler.characters(ch, offset, limit - offset); } } // Output escaped characters if required. Non-ASCII characters // within HTML attributes should _NOT_ be escaped. else if (_escapeChars) { for (i = off; i < limit; i++) { switch (ch[i]) { case '&': _saxHandler.characters(ch, offset, i - offset); _saxHandler.characters(AMP, 0, AMP_length); offset = i + 1; break; case '"': _saxHandler.characters(ch, offset, i - offset); _saxHandler.characters(QUOTE, 0, QUOTE_length); offset = i + 1; break; case '<': _saxHandler.characters(ch, offset, i - offset); _saxHandler.characters(LT, 0, LT_length); offset = i + 1; break; case '>': _saxHandler.characters(ch, offset, i - offset); _saxHandler.characters(GT, 0, GT_length); offset = i + 1; break; case '\u00a0': _saxHandler.characters(ch, offset, i - offset); _saxHandler.characters(NBSP, 0, NBSP_length); offset = i + 1; break; } // !!! not finished yet (more chars need escaping) } } if (offset < limit) { _saxHandler.characters(ch, offset, limit - offset); } } catch (SAXException e) { throw new TransletException(e); } } /** * Start an element in the output document. This might be an XML * element (<elem>data</elem> type) or a CDATA section. */ public void startElement(String elementName) throws TransletException { // bug fix # 1499, GTM. if (_outputType == TEXT) return; try { // Close any open start tag if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } // If we don't know the output type yet we need to examine // the very first element to see if it is "html". if (_outputType == UNKNOWN) { if (elementName.toLowerCase().equals("html")) { setType(HTML); setIndent(true); _escapeChars = true; } else { setType(XML); emitHeader(); _escapeChars = true; } } _depth++; _elementName = elementName; _attributes.clear(); _startTagOpen = true; _qnameStack.push(elementName); // Insert <META> tag directly after <HEAD> element in HTML doc if (_outputType == HTML) { if (elementName.toLowerCase().equals("head")) { _headTagOpen = true; } } } catch (SAXException e) { throw new TransletException(e); } } /** * This method escapes special characters used in attribute values */ private String escapeChars(String value) { int i; char[] ch = value.toCharArray(); int limit = ch.length; int offset = 0; StringBuffer buf = new StringBuffer(); for (i = 0; i < limit; i++) { switch (ch[i]) { case '&': buf.append(ch, offset, i - offset); buf.append(AMP); offset = i + 1; break; case '"': buf.append(ch, offset, i - offset); buf.append(QUOTE); offset = i + 1; break; case '<': buf.append(ch, offset, i - offset); buf.append(LT); offset = i + 1; break; case '>': buf.append(ch, offset, i - offset); buf.append(GT); offset = i + 1; break; case '\n': buf.append(ch, offset, i - offset); buf.append(CRLF); offset = i + 1; break; } } if (offset < limit) { buf.append(ch, offset, limit - offset); } return(buf.toString()); } /** * Put an attribute and its value in the start tag of an element. * Signal an exception if this is attempted done outside a start tag. */ public void attribute(final String name, final String value) throws TransletException { // bug fix #1499, GTM if (_outputType == TEXT) return; if (_startTagOpen) { // Intercept namespace declarations and handle them separately if (name.startsWith("xmlns")) { if (name.length() == 5) namespace(EMPTYSTRING,value); else namespace(name.substring(6),value); } else { _attributes.add(name,escapeChars(value)); } } else if (_cdataTagOpen) { throw new TransletException("attribute '"+name+"' within CDATA"); } else { throw new TransletException("attribute '"+name+ "' outside of element"); } } /** * End an element or CDATA section in the output document */ public void endElement(String elementName) throws TransletException { // bug fix #1499, GTM if (_outputType == TEXT) return; try { // Close any open element if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } final String qname = (String)(_qnameStack.pop()); _saxHandler.endElement(null, null, qname); popNamespaces(); _depth--; } catch (SAXException e) { throw new TransletException(e); } } /** * Send a HTML-style comment to the output document */ public void comment(String comment) throws TransletException { try { // Close any open element before emitting comment if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } // Set output type to XML (the default) if still unknown. if (_outputType == UNKNOWN) { setType(XML); emitHeader(); _escapeChars = true; } // ...and then output the comment. characters(BEGCOMM); characters(comment); characters(ENDCOMM); } catch (SAXException e) { throw new TransletException(e); } } /** * Send a processing instruction to the output document */ public void processingInstruction(String target, String data) throws TransletException { try { // Close any open element if (_startTagOpen) { closeStartTag(); } else if (_cdataTagOpen) { characters(ENDCDATA); _cdataTagOpen = false; } // Pass the processing instruction to the SAX handler _saxHandler.processingInstruction(target, data); } catch (SAXException e) { throw new TransletException(e); } } /** * Initialize namespace stacks */ private void initNamespaces() { _namespaces = new Hashtable(); _nodeStack = new Stack(); _prefixStack = new Stack(); // Define the default namespace (initially maps to "" uri) Stack stack = new Stack(); _namespaces.put(EMPTYSTRING, stack); stack.push(EMPTYSTRING); _prefixStack.push(EMPTYSTRING); _nodeStack.push(new Integer(-1)); _depth = 0; } /** * Declare a prefix to point to a namespace URI */ private void pushNamespace(String prefix, String uri) throws SAXException { if (prefix.equals(XML_PREFIX)) return; Stack stack; // Get the stack that contains URIs for the specified prefix if ((stack = (Stack)_namespaces.get(prefix)) == null) { stack = new Stack(); _namespaces.put(prefix, stack); } // Quit now if the URI the prefix currently maps to is the same as this if (!stack.empty() && uri.equals(stack.peek())) return; // Put this URI on top of the stack for this prefix stack.push(uri); _prefixStack.push(prefix); _nodeStack.push(new Integer(_depth)); _saxHandler.startPrefixMapping(prefix, uri); } /** * Undeclare the namespace that is currently pointed to by a given prefix */ private void popNamespace(String prefix) throws SAXException { if (prefix.equals(XML_PREFIX)) return; Stack stack; if ((stack = (Stack)_namespaces.get(prefix)) != null) { stack.pop(); _saxHandler.endPrefixMapping(prefix); } } /** * Pop all namespace definitions that were delcared by the current element */ private void popNamespaces() throws TransletException { try { while (true) { if (_nodeStack.isEmpty()) return; Integer i = (Integer)(_nodeStack.peek()); if (i.intValue() != _depth) return; _nodeStack.pop(); popNamespace((String)_prefixStack.pop()); } } catch (SAXException e) { throw new TransletException(e); } } /** * Use a namespace prefix to lookup a namespace URI */ private String lookupNamespace(String prefix) { final Stack stack = (Stack)_namespaces.get(prefix); return stack != null && !stack.isEmpty() ? (String)stack.peek() : null; } /** * Send a namespace declaration in the output document. The namespace * declaration will not be include if the namespace is already in scope * with the same prefix. */ public void namespace(final String prefix, final String uri) throws TransletException { try { if (_startTagOpen) pushNamespace(prefix, uri); else if (_cdataTagOpen) throw new TransletException("namespace declaration within "+ "CDATA element"); else throw new TransletException("namespace declaration '"+prefix+ "'='"+uri+"' outside of element"); } catch (SAXException e) { throw new TransletException(e); } } /** * Takes a qname as a string on the format prefix:local-name and * returns a strig with the expanded QName on the format uri:local-name. */ private String expandQName(String withPrefix) { int col = withPrefix.lastIndexOf(':'); if (col == -1) return(withPrefix); final String prefix = withPrefix.substring(0,col); final String local = withPrefix.substring(col+1,withPrefix.length()); final String uri = lookupNamespace(prefix); if (uri == null) return(local); else if (uri == EMPTYSTRING) return(local); else return(uri+":"+local); } }