blob: 22eb12c5f40883bd7af73d2235441a47478a3e10 [file] [log] [blame]
/*
* @(#)$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
* @author Seth Ford <seth.ford@fmr.com>
*
*/
package org.apache.xalan.xsltc.runtime;
import java.io.*;
import java.util.Stack;
import java.util.Enumeration;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.xalan.xsltc.*;
public final class TextOutput implements TransletOutputHandler, Constants {
// These are the various output types we handle
public static final int UNKNOWN = 0; // determine type from output contents
public static final int XML = 1;
public static final int HTML = 2;
public static final int TEXT = 3;
public static final int QNAME = 4; // no special handling
// These parameters are set by the <xsl:output> element, or by the
// get/setOutputProperty() methods in TrAX
private int _outputType = UNKNOWN;
private String _encoding;
private String _mediaType = "text/html";
private String _doctypeSystem = null;
private String _doctypePublic = null;
private boolean _escapeChars = false;
private boolean _startTagOpen = false;
private boolean _headTagOpen = false;
private boolean _cdataTagOpen = false;
private boolean _is8859Encoded = false;
// Contains all elements that should be output as CDATA sections
private Hashtable _cdata = null;
private static final char[] AMP = "&amp;".toCharArray();
private static final char[] LT = "&lt;".toCharArray();
private static final char[] GT = "&gt;".toCharArray();
private static final char[] CRLF = "&#xA;".toCharArray();
private static final char[] QUOTE = "&quot;".toCharArray();
private static final char[] NBSP = "&nbsp;".toCharArray();
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 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 int BEGCOMM_length = BEGCOMM.length;
private static final int ENDCOMM_length = ENDCOMM.length;
private static final String HREF_STR = "href";
private static final String CITE_STR = "cite";
private static final String SRC_STR = "src";
private static final String CHAR_ESC_START = "&#";
private static final String CDATA_ESC_START = "]]>&#";
private static final String CDATA_ESC_END = ";<![CDATA[";
private AttributesImpl _attributes = new AttributesImpl();
private String _elementName = null;
// Each entry (prefix) in this hashtable points to a Stack of URIs
private Hashtable _namespaces;
// The top of this stack contains an id of the element that last declared
// a namespace. Used to ensure prefix/uri map scopes are closed correctly
private Stack _nodeStack;
// The top of this stack is the prefix that was last mapped to an URI
private Stack _prefixStack;
// The top of this stack contains the QName of the currently open element
private Stack _qnameStack;
// The top of this stack contains the element id of the last element whose
// contents should be output as CDATA sections.
private Stack _cdataStack;
// Holds the current tree depth (see startElement() and endElement()).
private int _depth = 0;
// Reference to the SAX2 handlers that consume this handler's output
private ContentHandler _saxHandler;
private LexicalHandler _lexHandler;
/**
* Creates a new translet output post-processor
*
* @param handler A SAX2 handler to consume the generated SAX events
* @throws IOException
*/
public TextOutput(ContentHandler handler) throws IOException {
_saxHandler = handler;
init();
}
/**
* Creates a new translet output post-processor
*
* @param handler A SAX2 handler to consume the generated SAX events
* @param encoding The default encoding to use (set in <xsl:output>)
* @throws IOException
*/
public TextOutput(ContentHandler handler, String encoding)
throws IOException {
_saxHandler = handler;
init();
_encoding = encoding;
_is8859Encoded = _encoding.equalsIgnoreCase("iso-8859-1");
}
/**
* Creates a new translet output post-processor
*
* @param handler A SAX2 handler to consume the generated SAX events
* @param encoding The default encoding to use (set in <xsl:output>)
* @throws IOException
*/
public TextOutput(ContentHandler sax, LexicalHandler lex, String encoding)
throws IOException {
_saxHandler = sax;
_lexHandler = lex;
init();
_encoding = encoding;
_is8859Encoded = _encoding.equalsIgnoreCase("iso-8859-1");
}
/**
* Initialise global variables
*/
private void init() throws IOException {
// Reset all output configuration from <xsl:output>
_outputType = UNKNOWN;
_encoding = "UTF-8";
_mediaType = "text/html";
// Reset all internal variables and tables
_escapeChars = false;
_startTagOpen = false;
_cdataTagOpen = false;
_qnameStack = new Stack();
_cdataStack = new Stack();
_cdataStack.push(new Integer(-1)); // push dummy value
// Reset our internal namespace handling
initNamespaces();
}
/**
* Closes the output stream
*/
public void close() {
if ((_saxHandler != null) &&
(_saxHandler instanceof DefaultSAXOutputHandler)) {
((DefaultSAXOutputHandler)_saxHandler).close();
}
}
/**
* This method is used internally when the output type was initially
* undefined and the type is set (by this handler) based on the contents
* of the output. Set the default values for some output paramters.
*/
private void setTypeInternal(int type) {
if (type == XML) {
_escapeChars = true;
}
else if (type == HTML) {
setIndent(true);
_escapeChars = true;
}
setType(type);
}
/**
* Emit header through the SAX handler
*/
private void emitHeader() throws SAXException {
// Output HTML header as META element
if (_outputType == HTML) {
AttributeList attrs = new AttributeList();
attrs.add("http-equiv", "Content-Type");
attrs.add("content", _mediaType+"; charset="+_encoding);
_saxHandler.startElement(EMPTYSTRING, EMPTYSTRING, "meta", attrs);
_saxHandler.endElement(EMPTYSTRING, EMPTYSTRING, "meta");
}
}
/**
* 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;
// Now is time to send the startElement event
_saxHandler.startElement(getNamespaceURI(_elementName, true),
getLocalName(_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 {
// Set output type to XML (the default) if still unknown.
if (_outputType == UNKNOWN) setTypeInternal(XML);
// Save old escaping setting (for the return value) and set new value
boolean oldSetting = _escapeChars;
_escapeChars = escape;
// Characters are never escaped in output mode 'text'
if (_outputType == TEXT) _escapeChars = false;
return(oldSetting);
}
/**
* Starts the output document. Outputs the document header if the
* output type is set to XML.
*/
public void startDocument() throws TransletException {
try {
_saxHandler.startDocument();
// Output escaping is _ALWAYS_ enabled for XML output
if (_outputType == XML) _escapeChars = true;
} catch (SAXException e) {
throw new TransletException(e);
}
}
/**
* Ends the document output.
*/
public void endDocument() throws TransletException {
try {
// Set output type to XML (the default) if still unknown.
if (_outputType == UNKNOWN) setTypeInternal(XML);
// Close any open start tag
if (_startTagOpen) closeStartTag();
if (_cdataTagOpen) closeCDATA();
// 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();
characters(ch, 0, ch.length);
}
/**
* Utility method - pass a whole character array to the SAX handler
*/
private void characters(char[] ch) throws SAXException {
characters(ch, 0, ch.length);
}
/**
* Utility method - pass a whole charactes as CDATA to SAX handler
*/
private void startCDATA(char[] ch, int off, int len) throws SAXException {
final int limit = off + len;
int offset = off;
// Output start bracket - "<![CDATA["
_saxHandler.characters(BEGCDATA, 0, BEGCDATA.length);
// Detect any occurence of "]]>" in the character array
for (int i = offset; i < limit-2; i++) {
if (ch[i] == ']' && ch[i+1] == ']' && ch[i+2] == '>') {
_saxHandler.characters(ch, offset, i - offset);
_saxHandler.characters(CNTCDATA, 0, CNTCDATA.length);
offset = i+3;
i=i+2; // Skip next chars ']' and '>'.
}
}
// Output the remaining characters
if (offset < limit) _saxHandler.characters(ch, offset, limit - offset);
_cdataTagOpen = true;
}
private void closeCDATA() throws SAXException {
// Output closing bracket - "]]>"
_saxHandler.characters(ENDCDATA, 0, ENDCDATA.length);
_cdataTagOpen = false;
}
/**
* Send characters to the output document
*/
public void characters(char[] ch, int off, int len)
throws TransletException {
try {
switch(_outputType) {
case UNKNOWN: // Set type to XML and fall through
setTypeInternal(XML);
case XML:
// Close any open start tag
if (_startTagOpen) closeStartTag();
// 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. Other
// special characters/sequences are _NOT_ escaped within CDATA.
Integer I = (Integer)_cdataStack.peek();
if ((I.intValue() == _depth) && (!_cdataTagOpen)) {
startCDATA(ch, off, len);
}
else if (_escapeChars) {
if (_cdataTagOpen) {
escapeCDATA(ch, off, len);
} else {
escapeCharacters(ch, off, len);
}
}
else {
_saxHandler.characters(ch, off, len);
}
return;
case HTML:
// Close any open start tag
if (_startTagOpen) closeStartTag();
// Output escaped characters if required. Non-ASCII characters
// within HTML attributes should _NOT_ be escaped.
if (_escapeChars) {
if (!_qnameStack.isEmpty()) {
String qname = (String)_qnameStack.peek();
qname = qname.toLowerCase();
if ((qname.equals("style"))||(qname.equals("script"))) {
_saxHandler.characters(ch, off, len);
return;
}
}
escapeCharacters(ch, off, len);
}
// Output the chracters as the are
else {
_saxHandler.characters(ch, off, len);
}
return;
case TEXT:
_saxHandler.characters(ch, off, len);
return;
}
}
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 {
try {
switch(_outputType) {
case UNKNOWN:
// If we don't know the output type yet we need to examine
// the very first element to see if it is "html".
if (elementName.toLowerCase().equals("html"))
setTypeInternal(HTML);
else
setTypeInternal(XML);
startElement(elementName);
return;
case XML:
// Close any open start tag
if (_startTagOpen) closeStartTag();
if (_cdataTagOpen) closeCDATA();
// Handle document type declaration (for first element only)
if (_lexHandler != null) {
if (_doctypeSystem != null)
_lexHandler.startDTD(elementName,
_doctypePublic,_doctypeSystem);
_lexHandler = null;
}
_depth++;
_elementName = elementName;
_attributes.clear();
_startTagOpen = true;
_qnameStack.push(elementName);
if ((_cdata != null) && (_cdata.get(elementName) != null))
_cdataStack.push(new Integer(_depth));
return;
case HTML:
// Close any open start tag
if (_startTagOpen) closeStartTag();
// Handle document type declaration (for first element only)
if (_lexHandler != null) {
if ((_doctypeSystem != null) || (_doctypePublic != null))
_lexHandler.startDTD(elementName,
_doctypePublic,_doctypeSystem);
_lexHandler = null;
}
_depth++;
_elementName = elementName;
_attributes.clear();
_startTagOpen = true;
_qnameStack.push(elementName);
// Insert <META> tag directly after <HEAD> element in HTML doc
if (elementName.toLowerCase().equals("head"))
_headTagOpen = true;
return;
case TEXT:
// Do not output element tags if output mode is 'text'
return;
}
}
catch (SAXException e) {
throw new TransletException(e);
}
}
/**
* Utility method - escape special characters and pass to SAX handler
*/
private void escapeCharacters(char[] ch, int off, int len)
throws SAXException {
int limit = off + len;
int offset = off;
if (limit > ch.length) limit = ch.length;;
// Step through characters and escape all special characters
for (int i = off; i < limit; i++) {
char current = ch[i];
switch (current) {
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(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;
default:
if ( (current >= '\u007F' && current < '\u00A0') ||
(_is8859Encoded && (current > '\u00FF')) )
{
StringBuffer buf = new StringBuffer(CHAR_ESC_START);
buf.append(Integer.toString((int)ch[i]));
buf.append(';');
final String esc = buf.toString();
final char[] chars = esc.toCharArray();
final int strlen = esc.length();
_saxHandler.characters(ch, offset, i - offset);
_saxHandler.characters(chars, 0, strlen);
offset = i + 1;
}
}
}
// Output remaining characters (that do not need escaping).
if (offset < limit) _saxHandler.characters(ch, offset, limit - offset);
}
/**
* Utility method - escape special characters and pass to SAX handler
*/
private void escapeCDATA(char[] ch, int off, int len)
throws SAXException {
int limit = off + len;
int offset = off;
if (limit > ch.length) limit = ch.length;;
// Step through characters and escape all special characters
for (int i = off; i < limit; i++) {
if (ch[i] > '\u00ff') {
StringBuffer buf = new StringBuffer(CDATA_ESC_START);
buf.append(Integer.toString((int)ch[i]));
buf.append(CDATA_ESC_END);
final String esc = buf.toString();
final char[] chars = esc.toCharArray();
final int strlen = esc.length();
_saxHandler.characters(ch, offset, i - offset);
_saxHandler.characters(chars, 0, strlen);
offset = i + 1;
}
}
// Output remaining characters (that do not need escaping).
if (offset < limit) _saxHandler.characters(ch, offset, limit - offset);
}
/**
* This method escapes special characters used in attribute values
*/
private String escapeString(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());
}
private String makeHHString(int i) {
String s = Integer.toHexString(i).toUpperCase();
if (s.length() == 1) {
s = "0"+s;
}
return s;
}
/**
* This method escapes special characters used in HTML attribute values
*/
private String escapeAttr(String base) {
final int len = base.length() - 1;
final String str = "&quot;";
int pos;
char[] ch = base.toCharArray();
StringBuffer buf = new StringBuffer();
for(int i=0; i<base.length(); i++){
if (ch[i] <= 0x20) {
buf.append('%');
buf.append(makeHHString(ch[i]));
}
else if (ch[i] > '\u007F') {
int high = (ch[i] >> 6) | 0xC0;
int low = (ch[i] & 0x3F) | 0x80; // First 6 bits + high bit
buf.append('%');
buf.append(makeHHString(high));
buf.append('%');
buf.append(makeHHString(low));
}
else {
// These chars are reserved or unsafe in URLs
// pg 196, 'HTML, The Definitive Guide" C. Musciano, et. al
switch (ch[i]) {
case '\u007F' :
case '\u0022' :
case '\u007B' :
case '\u007D' :
case '\u007C' :
case '\\' :
case '\t' :
case '\u005E' :
case '\u007E' :
case '\u005B' :
case '\u005D' :
case '\u0060' :
buf.append('%');
buf.append(Integer.toHexString((int)ch[i]));
break;
case '\u0026' :
//bug fix for customer/murphy3: buf.append("&amp;");
buf.append("&");
break;
default:
buf.append(ch[i]); break;
}
}
}
base = buf.toString();
while ((pos = base.indexOf('"')) > -1) {
if (pos == 0) {
final String after = base.substring(1);
base = str + after;
}
else if (pos == len) {
final String before = base.substring(0, pos);
base = before + str;
}
else {
final String before = base.substring(0, pos);
final String after = base.substring(pos+1);
base = before + str + after;
}
}
return base;
}
/**
* Replaces whitespaces in a URL with '%20'
*/
private String quickAndDirtyUrlEncode(String base) {
final String pst20 = "%20";
final int len = base.length() - 1;
int pos;
// Slow, very slow indeed
while ((pos = base.indexOf(' ')) > -1) {
if (pos == 0) {
final String after = base.substring(1);
base = pst20 + after;
}
else if (pos == len) {
final String before = base.substring(0, pos);
base = before + pst20;
}
else {
final String before = base.substring(0, pos);
final String after = base.substring(pos+1);
base = before + pst20 + after;
}
}
return base;
}
/**
* Returns the URI of an element or attribute. Note that default namespaces
* do not apply directly to attributes.
*/
private String getNamespaceURI(String qname, boolean isElement)
throws TransletException
{
String uri = EMPTYSTRING;
int col = qname.lastIndexOf(':');
final String prefix = (col > 0) ? qname.substring(0, col) : EMPTYSTRING;
if (prefix != EMPTYSTRING || isElement) {
uri = lookupNamespace(prefix);
if (uri == null && !prefix.equals(XMLNS_PREFIX)) {
BasisLibrary.runTimeError(BasisLibrary.NAMESPACE_PREFIX_ERR,
qname.substring(0, col));
}
}
return uri;
}
/**
* Returns the local name of a qualified name. If the name has no prefix
* then return null.
*/
private static String getLocalName(String qname) throws TransletException {
final int col = qname.lastIndexOf(':');
return (col > 0) ? qname.substring(col + 1) : null;
}
/**
* TODO: This method is a HACK! Since XSLTC does not have access to the
* XML file, it sometimes generates a NS prefix of the form "ns?" for
* an attribute. If at runtime, when the qname of the attribute is
* known, another prefix is specified for the attribute, then we can get
* a qname of the form "ns?:otherprefix:name". This function patches the
* qname by simply ignoring "otherprefix".
*/
private static String patchQName(String qname) throws TransletException {
final int lastColon = qname.lastIndexOf(':');
if (lastColon > 0) {
final int firstColon = qname.indexOf(':');
if (firstColon != lastColon) {
return qname.substring(0, firstColon) + qname.substring(lastColon);
}
}
return qname;
}
/**
* 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(String name, final String value)
throws TransletException {
if (_outputType == TEXT) return;
final String patchedName = patchQName(name);
final String localName = getLocalName(patchedName);
final String uri = getNamespaceURI(patchedName, false);
final int index = (localName == null) ?
_attributes.getIndex(name) : /* don't use patchedName */
_attributes.getIndex(uri, localName);
switch(_outputType) {
case XML:
if (!_startTagOpen) {
BasisLibrary.runTimeError(BasisLibrary.STRAY_ATTRIBUTE_ERR, patchedName);
}
/*
System.err.println("TextOutput.attribute() uri = " + uri
+ " localname = " + localName
+ " qname = " + name
+ "\n value = " + value
+ " escapeString(value) = " + escapeString(value));
*/
// Output as namespace declaration
if (name.startsWith(XMLNS_PREFIX)) {
namespace(name.length() > 6 ? name.substring(6) : EMPTYSTRING, value);
}
else {
if (index >= 0) { // Duplicate attribute?
_attributes.setAttribute(index, uri, localName, patchedName, "CDATA",
escapeString(value));
}
else {
_attributes.addAttribute(uri, localName, patchedName, "CDATA",
escapeString(value));
}
}
break;
case HTML:
if (!_startTagOpen) {
BasisLibrary.runTimeError(BasisLibrary.STRAY_ATTRIBUTE_ERR,name);
}
/*
* The following is an attempt to escape an URL stored in a href
* attribute of HTML output. Normally URLs should be encoded at
* the time they are created, since escaping or unescaping a
* completed URI might change its semantics. We limit or escaping
* to include space characters only - and nothing else. This is for
* two reasons: (1) performance and (2) we want to make sure that
* we do not change the meaning of the URL.
*/
final String tmp = name.toLowerCase();
if (tmp.equals(HREF_STR) || tmp.equals(SRC_STR) || tmp.equals(CITE_STR)) {
if (index >= 0) {
_attributes.setAttribute(index, EMPTYSTRING, EMPTYSTRING, name,
"CDATA", quickAndDirtyUrlEncode(escapeAttr(value)));
}
else {
_attributes.addAttribute(EMPTYSTRING, EMPTYSTRING, name, "CDATA",
quickAndDirtyUrlEncode(escapeAttr(value)));
}
}
else {
if (index >= 0) {
_attributes.setAttribute(index, EMPTYSTRING, EMPTYSTRING,
name, "CDATA", escapeNonURLAttr(value));
}
else {
_attributes.addAttribute(EMPTYSTRING, EMPTYSTRING,
name, "CDATA", escapeNonURLAttr(value));
}
}
break;
}
}
/**
* Escape non ASCII characters (> u007F) as &#XXX; entities.
*/
private String escapeNonURLAttr(String base) {
final int len = base.length() - 1;
char[] ch = base.toCharArray();
StringBuffer buf = new StringBuffer();
for(int i=0; i<base.length(); i++){
if (ch[i] > '\u007F') {
buf.append(CHAR_ESC_START);
buf.append(Integer.toString((int)ch[i]));
buf.append(';');
}
else {
buf.append(ch[i]);
}
}
base = buf.toString();
return base;
}
/**
* End an element or CDATA section in the output document
*/
public void endElement(String elementName) throws TransletException {
try {
switch(_outputType) {
case TEXT:
// Do not output element tags if output mode is 'text'
return;
case XML:
// Close any open element
if (_startTagOpen) closeStartTag();
if (_cdataTagOpen) closeCDATA();
final String qname = (String) _qnameStack.pop();
_saxHandler.endElement(getNamespaceURI(qname, true),
getLocalName(qname), qname);
popNamespaces();
if (((Integer)_cdataStack.peek()).intValue() == _depth)
_cdataStack.pop();
_depth--;
return;
case HTML:
// Close any open element
if (_startTagOpen) closeStartTag();
_saxHandler.endElement(EMPTYSTRING, EMPTYSTRING,
(String)(_qnameStack.pop()));
popNamespaces();
_depth--;
return;
}
} 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();
if (_cdataTagOpen) closeCDATA();
// Set output type to XML (the default) if still unknown.
if (_outputType == UNKNOWN) setTypeInternal(XML);
// ...and then output the comment.
_saxHandler.characters(BEGCOMM, 0, BEGCOMM_length);
characters(comment);
_saxHandler.characters(ENDCOMM, 0, ENDCOMM_length);
}
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();
if (_cdataTagOpen) closeCDATA();
// Handle document type declaration (only HTML at this point)
if ((_lexHandler != null) && (_outputType == HTML)) {
if ((_doctypeSystem != null) || (_doctypePublic != null))
_lexHandler.startDTD("HTML",_doctypePublic,_doctypeSystem);
_lexHandler = null;
}
// 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;
_namespaces.put(EMPTYSTRING, stack = new Stack());
stack.push(EMPTYSTRING);
_prefixStack.push(EMPTYSTRING);
_namespaces.put(XML_PREFIX, stack = new Stack());
stack.push("http://www.w3.org/XML/1998/namespace");
_prefixStack.push(XML_PREFIX);
_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 {
// Prefixes "xml" and "xmlns" cannot be redefined
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));
// Inform the SAX handler
_saxHandler.startPrefixMapping(prefix, escapeString(uri));
}
/**
* Undeclare the namespace that is currently pointed to by a given prefix
*/
private void popNamespace(String prefix) throws SAXException {
// Prefixes "xml" and "xmlns" cannot be redefined
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 ((prefix == EMPTYSTRING) && (uri == EMPTYSTRING)) return;
BasisLibrary.runTimeError(BasisLibrary.STRAY_NAMESPACE_ERR,
prefix, uri);
}
}
catch (SAXException e) {
throw new TransletException(e);
}
}
/************************************************************************
* The following are all methods for configuring the output settings
************************************************************************/
/**
* Set the output type. The type must be wither TEXT, XML or HTML.
*/
public void setType(int type) {
try {
_outputType = type;
if ((_outputType == HTML) || (_outputType == XML))
_escapeChars = true;
if (_encoding == null)
_encoding = "UTF-8";
if (_saxHandler instanceof DefaultSAXOutputHandler)
((DefaultSAXOutputHandler)_saxHandler).setOutputType(type);
}
catch (SAXException e) { }
}
/**
* 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.
*/
public void omitHeader(boolean value) {
if (_saxHandler instanceof DefaultSAXOutputHandler) {
((DefaultSAXOutputHandler)_saxHandler).omitHeader(value);
}
}
/**
* Set the XML output document version - should be 1.0
*/
public void setVersion(String version) {
if (_saxHandler instanceof DefaultSAXOutputHandler) {
((DefaultSAXOutputHandler)_saxHandler).setVersion(version);
}
}
/**
* Set the XML standalone attribute - must be "yes" or "no"
*/
public void setStandalone(String standalone) {
if (_saxHandler instanceof DefaultSAXOutputHandler) {
((DefaultSAXOutputHandler)_saxHandler).setStandalone(standalone);
}
}
/**
* Set the output document system/public identifiers
*/
public void setDoctype(String system, String pub) {
_doctypeSystem = system;
_doctypePublic = pub;
}
/**
* Set the output media type - only relevant for HTML output
*/
public void setMediaType(String mediaType) {
// This value does not have to be passed to the SAX handler. This
// handler creates the HTML <meta> tag in which the media-type
// (MIME-type) will be used.
_mediaType = mediaType;
}
/**
* 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 setCdataElements(Hashtable elements) {
_cdata = elements;
}
}