/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xsl.java;

import com.caucho.java.JavaWriter;
import com.caucho.log.Log;
import com.caucho.util.CharBuffer;
import com.caucho.util.CompileException;
import com.caucho.util.L10N;
import com.caucho.util.LineCompileException;
import com.caucho.xml.QName;
import com.caucho.xml.XmlChar;
import com.caucho.xpath.Expr;
import com.caucho.xpath.NamespaceContext;
import com.caucho.xpath.XPath;
import com.caucho.xpath.expr.NumericExpr;
import com.caucho.xpath.pattern.*;
import com.caucho.xsl.JavaGenerator;
import com.caucho.xsl.XslParseException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Logger;

/**
 * Represents any XSL node from the stylesheet.
 */
public abstract class XslNode {
  static final L10N L = new L10N(XslNode.class);
  static final Logger log = Log.open(XslNode.class);

  protected String _systemId;
  protected String _filename;
  protected int _startLine;
  protected int _endLine;
  
  protected JavaGenerator _gen;

  protected QName _name;
  protected XslNode _parent;

  protected ArrayList<XslNode> _children;
  protected NamespaceContext _matchNamespace;
  protected NamespaceContext _outputNamespace;

  private int _varCount;

  protected XslNode()
  {
  }

  /**
   * Sets the Java generator.
   */
  public void setGenerator(JavaGenerator gen)
  {
    _gen = gen;
  }

  /**
   * Returns the qname of the node.
   */
  public QName getQName()
  {
    return _name;
  }

  /**
   * Sets the node's qname
   */
  public void setQName(QName name)
  {
    _name = name;
  }

  /**
   * Returns the qname of the node.
   */
  public String getTagName()
  {
    if (_name != null)
      return _name.getName();
    else
      return getClass().getName();
  }
  
  /**
   * Returns the parent node.
   */
  public XslNode getParent()
  {
    return _parent;
  }

  /**
   * Sets the parent node
   */
  public void setParent(XslNode parent)
  {
    _parent = parent;

    if (parent != null) {
      _matchNamespace = parent.getMatchNamespace();
      _outputNamespace = parent.getOutputNamespace();
    }
  }

  /**
   * Add variable.
   */
  public void addVariableCount()
  {
    if (_parent != null)
      _parent._varCount++;
  }

  /**
   * Sets the start location of the node.
   */
  public void setStartLocation(String systemId, String filename, int line)
  {
    _systemId = systemId;
    _filename = filename;
    _startLine = line;
  }

  /**
   * Sets the end location of the node.
   */
  public void setEndLocation(String filename, int line)
  {
    if (_filename != null && _filename.equals(filename))
      _endLine = line;
  }

  /**
   * Gets the system id of the node
   */
  public String getSystemId()
  {
    return _systemId;
  }

  /**
   * Gets the filename of the node
   */
  public String getFilename()
  {
    return _filename;
  }

  /**
   * Gets the starting line number
   */
  public int getStartLine()
  {
    return _startLine;
  }

  /**
   * Gets the ending line number
   */
  public int getEndLine()
  {
    return _endLine;
  }

  /**
   * Returns the base URI.
   */
  public String getBaseURI()
  {
    return _filename;
  }

  /**
   * Returns the namespaces.
   */
  public NamespaceContext getMatchNamespace()
  {
    return _matchNamespace;
  }

  /**
   * Returns the namespaces.
   */
  public NamespaceContext getOutputNamespace()
  {
    return _outputNamespace;
  }

  /**
   * Returns the matching node in the namespace.
   */
  public String getNamespace(String prefix)
  {
    return NamespaceContext.find(getOutputNamespace(), prefix);
  }

  /**
   * Adds an attribute.
   */
  public void addAttribute(QName name, String value)
    throws XslParseException
  {
    if (name.getName().startsWith("xmlns")) {
      addNamespaceAttribute(name, value);
      return;
    }
    
    if (name.getName().startsWith("xml"))
      return;
    
    throw error(L.l("attribute `{0}' is not allowed in <{1}>.",
                    name.getName(), getTagName()));
  }

  /**
   * Adds an attribute.
   */
  protected void addNamespaceAttribute(QName name, String url)
    throws XslParseException
  {
    // Note: according to the spec, the default namespace is not used

    /*
    if (url.equals(JavaGenerator.XSLNS) || url.equals(JavaGenerator.XTPNS))
      return;

    if (url.startsWith("quote:"))
      url = url.substring(6);
    */
    
    String localName = name.getLocalName();

    _outputNamespace = new NamespaceContext(_outputNamespace, localName, url);

    if (! localName.equals("xmlns")) {
      // xsl/04w3
      _matchNamespace = new NamespaceContext(_matchNamespace, localName, url);
    }
  }

  /**
   * Called after all the attributes from the tag.
   */
  public void endAttributes()
    throws XslParseException
  {
  }

  /**
   * Adds text.
   */
  public void addText(String text)
    throws XslParseException
  {
    for (int i = 0; i < text.length(); i++) {
      char ch = text.charAt(i);

      if (! XmlChar.isWhitespace(ch))
        throw error(L.l("Text is not allowed in <{0}> at `{1}'.",
                        _name.getName(), text));
    }
  }

  /**
   * Adds a child node.
   */
  public void addChild(XslNode node)
    throws XslParseException
  {
    if (node == null)
      return;
    
    if (_children == null)
      _children = new ArrayList<XslNode>();

    _children.add(node);
  }

  /**
   * Called when the tag closes.
   */
  public void endElement()
    throws Exception
  {
  }

  /**
   * Returns the children.
   */
  public ArrayList<XslNode> getChildren()
  {
    return _children;
  }

  /**
   * Returns true if there are any children.
   */
  public boolean hasChildren()
  {
    return _children != null && _children.size() > 0;
  }

  /**
   * Generates the code for the tag
   *
   * @param out the output writer for the generated java.
   */
  abstract public void generate(JavaWriter out)
    throws Exception;

  /**
   * Generates the code for the children.
   *
   * @param out the output writer for the generated java.
   */
  public void generateChildren(JavaWriter out)
    throws Exception
  {
    if (_children == null)
      return;

    for (int i = 0; i < _children.size(); i++) {
      XslNode child = _children.get(i);

      out.setLocation(child.getFilename(), child.getStartLine());
      
      child.generate(out);
    }

    popScope(out);
  }

  /**
   * Generates the prelude code for the tag
   *
   * @param out the output writer for the generated java.
   */
  public void generateDeclaration(JavaWriter out)
    throws Exception
  {
    generateDeclarationChildren(out);
  }

  /**
   * Generates the declaration code for the children.
   *
   * @param out the output writer for the generated java.
   */
  public void generateDeclarationChildren(JavaWriter out)
    throws Exception
  {
    if (_children == null)
      return;

    for (int i = 0; i < _children.size(); i++) {
      XslNode child = _children.get(i);

      child.generateDeclaration(out);
    }
  }

  /**
   * Prints an attribute value.
   */
  protected void printAttributeValue(JavaWriter out, String name, String value)
    throws Exception
  {
    out.print("out.attribute(");
    out.print(name == null ? "null" : ("\"" + name + "\""));
    out.print(", ");
    if (value == null)
      out.print("null");
    else {
      out.print("\"");
      out.printJavaString(value);
      out.print("\"");
    }
    out.println(");");
  }

  /**
   * Prints an attribute value.
   */
  protected void printAttributeValue(JavaWriter out, String value)
    throws Exception
  {
    if (value == null) {
      out.print("null");
      return;
    }

    if (value.indexOf("{") < 0) {
      out.print("\"");
      out.printJavaString(value);
      out.print("\"");
    }
    else {
      generateString(out, value);
    }
  }

  /**
   * Produces code to generate an attribute value template.  The same
   * code is used to produce a string ('a{b}c' -> "a" + b + "c") or a series of
   * print statements (',').
   *
   * @param string the source template
   * @param mode separator: either '+' or ','
   * @param elt the containing element.  Needed for namespaces.
   */
  void generateString(JavaWriter out, String string)
    throws Exception
  {
    int i = 0;
    boolean first = true;
    int length = string.length();
    CharBuffer cb = CharBuffer.allocate();

    for (; i < length; i++) {
      char ch = string.charAt(i);

      if (ch == '\n') {
	cb.append("\\n");
      }
      else if (ch == '"') {
	cb.append("\\\"");
      }
      else if (ch == '{' && i + 1 < length) {
	// {{ is treated as a single {
	if (string.charAt(i + 1) == '{') {
	  cb.append('{');
	  i++;
	}
	// the value is computed from an XPath expr
	else {
	  // print the gathered text if any
	  if (cb.length() > 0) {
	    out.print("out.print(\"");
	    out.printJavaString(cb.toString());
	    out.println("\");");
	  }

	  // scan the contents of '{' ... '}'
	  cb.clear();
	  for (i++; i < length && string.charAt(i) != '}'; i++)
	    cb.append(string.charAt(i));

	  printStringExpr(out, cb.toString());

	  cb.clear();
	  first = false;
	}
      }
      // }} is treated as a single }
      else if (ch == '}' && i + 1 < length) {
	if (string.charAt(i + 1) == '}') {
	  cb.append('}');
	  i++;
	}
	else
	  cb.append('}');
      }
      // <#= interpolates
      else if (i + 2 < length && ch == '<' && 
	       string.charAt(i + 1) == '#' &&
	       string.charAt(i + 2) == '=') {
	// print the gathered text if any
	if (cb.length() > 0) {
	  out.print("out.print(\"");
	  out.printJavaString(cb.toString());
	  out.println("\");");
	}

	// scan the contents of '{' ... '}'
	cb.clear();
	for (i += 3;
	     i + 1 < length && string.charAt(i) != '#' &&
	       string.charAt(i + 1) != '>';
	     i++)
	  cb.append(string.charAt(i));

	i++;
	
	// and add the results
	out.println("out.print(" + cb + ");");
	
	cb.clear();
	first = false;
      }
      else
	cb.append((char) ch);
    }

    // add any trailing text
    if (cb.length() > 0)
      out.println("out.print(\"" + cb + "\");");
  }

  /**
   * Produces code to generate an attribute value template.  The same
   * code is used to produce a string ('a{b}c' -> "a" + b + "c") or a series of
   * print statements (',').
   *
   * @param string the source template
   * @param mode separator: either '+' or ','
   * @param elt the containing element.  Needed for namespaces.
   */
  void generateString(JavaWriter out, String string, int mode)
    throws Exception
  {
    CharBuffer cb = new CharBuffer();
    int i = 0;
    boolean first = true;
    int length = string.length();

    for (; i < length; i++) {
      char ch = string.charAt(i);

      if (ch == '\n') {
	cb.append("\\n");
      }
      else if (ch == '"') {
	cb.append("\\\"");
      }
      else if (ch == '{' && i + 1 < length) {
	// {{ is treated as a single {
	if (string.charAt(i + 1) == '{') {
	  cb.append('{');
	  i++;
	}
	// the value is computed from an XPath expr
	else {
	  // print the gathered text if any
	  if (mode == ',') {
	    if (cb.length() > 0)
	      out.println("out.print(\"" + cb.toString() + "\");");
	  }
	  else {
	    if (! first)
	      out.print((char) mode);

	    if (cb.length() > 0) {
	      out.print("\"");
	      out.print(cb.toString());
	      out.print("\"");
	      out.print((char) mode);
	    }
	  }

	  // scan the contents of '{' ... '}'
	  cb.clear();
	  for (i++; i < length && string.charAt(i) != '}'; i++)
	    cb.append(string.charAt(i));

	  // and add the results
	  if (mode == ',')
	    printStringExpr(out, cb.toString());
	  else
	    stringExpr(out, cb.toString());
	  cb.clear();
	  first = false;
	}
      }
      // }} is treated as a single }
      else if (ch == '}' && i + 1 < length) {
	if (string.charAt(i + 1) == '}') {
	  cb.append('}');
	  i++;
	}
	else
	  cb.append('}');
      }
      // <#= interpolates
      else if (i + 2 < length && ch == '<' && 
	       string.charAt(i + 1) == '#' &&
	       string.charAt(i + 2) == '=') {
	// print the gathered text if any
	if (mode == ',') {
	  if (cb.length() > 0)
	    out.println("out.print(\"" + cb.toString() + "\");");
	}
	else {
	  if (! first)
	    out.print((char) mode);

	  if (cb.length() > 0) {
	    out.print("\"");
	    out.print(cb.toString());
	    out.print("\"");
	    out.print((char) mode);
	  }
	}

	// scan the contents of '{' ... '}'
	cb.clear();
	for (i += 3;
	     i + 1 < length && string.charAt(i) != '#' &&
	       string.charAt(i + 1) != '>';
	     i++)
	  cb.append(string.charAt(i));

	i++;
	
	// and add the results
	if (mode == ',')
	  out.println("out.print(" + cb + ");");
	else {
	  out.print("(" + cb + ")");
	}
	cb.clear();
	first = false;
      }
      else
	cb.append((char) ch);
    }

    // add any trailing text
    if (cb.length() > 0) {
      if (mode == ',') 
	out.println("out.print(\"" + cb + "\");");
      else {
	if (! first)
	  out.print((char) mode);

	out.print("\"" + cb + "\"");
      }
    } else if (first && mode == '+')
      out.print("\"\"");
  }

  /**
   * Prints a value-of expression
   */
  protected void printStringExpr(JavaWriter out, String exprString)
    throws Exception
  {
    if (exprString == null)
      return;
    
    int length = exprString.length();
    
    if (length == 0)
      return;

    AbstractPattern select = null;
    try {
      select = parseSelect(exprString);
    } catch (Exception e) {
      // this is expected in case where the expr is not a select expression
    }
    
    if (exprString.equals(".")) {
      out.println("out.valueOf(node);");
      return;
    }
    else if (exprString.charAt(0) == '@') {
      boolean isSimple = true;
      
      for (int i = 1; i < length; i++) {
        char ch = exprString.charAt(i);
        if (! XmlChar.isNameChar(ch) || ch == ':')
          isSimple = false;
      }

      if (isSimple) {
        out.println("if (node instanceof Element)");
        out.print("  out.print(((Element) node).getAttribute(\"");
        out.print(exprString.substring(1));
        out.println("\"));");
        return;
      }
    }
    else if (allowJavaSelect(select)) {
      int oldSelectDepth = _gen.getSelectDepth();

      String loop = "_xsl_loop" + _gen.generateId();
      _gen.setSelectLoopDepth(0);
      
      String ptr = printSelectBegin(out, select, true, loop);

      out.println("out.valueOf(" + ptr + ");");
      out.println("break " + loop + ";");

      int selectDepth = _gen.getSelectDepth();
      for (; oldSelectDepth < selectDepth; selectDepth--) {
        out.popDepth();
        out.println("}");
      }
      _gen.setSelectDepth(oldSelectDepth);

      return;
    }

    out.println("out.valueOf(_exprs[" + addExpr(exprString) +
		"].evalObject(node, " + _gen.getEnv() + "));");
  }

  protected void stringExpr(JavaWriter out, String exprString)
    throws Exception, XslParseException
  {
    out.print("_exprs[" + _gen.addExpr(parseExpr(exprString)) +
          "].evalString(node, " + getEnv() + ")");
  }

  protected void pushCall(JavaWriter out)
    throws IOException
  {
    out.println("{");
    out.pushDepth();

    int callDepth = _gen.pushCallDepth();
    
    out.println("Env _xsl_arg" + callDepth + " = XPath.createCall(env);");
  }

  protected void popCall(JavaWriter out)
    throws IOException
  {
    int callDepth = _gen.popCallDepth();
    out.println("_xsl_arg" + callDepth + ".free();");
    
    out.popDepth();
    out.println("}");
  }

  /**
   * Prints iterator code to start a select.
   */
  protected String printSelectBegin(JavaWriter out,
				    AbstractPattern select,
				    boolean isForEach, String loopVar)
    throws IOException, XslParseException
  {
    if (select == null)
      throw new NullPointerException();
    
    if (select instanceof FromContext
	&& ((FromContext) select).getCount() == 0)
      return "node";

    else if (select instanceof FromRoot)
      return "ownerDocument(node)";

    boolean useXPath = allowJavaSelect(select);
    
    String name = "node";

    if (! useXPath) {
      // punt and let XPath handle it.
      String iterName = "_xsl_iter" + _gen.generateId();
      
      String ptrName = "_xsl_ptr" + _gen.generateId();

      if (isForEach)
        out.println("env.setCurrentNode(node);");
      
      out.println("Iterator " + iterName + " = _select_patterns[" +
		  _gen.addSelect(select) + "].select(" + name + ", env);");
      
      if (loopVar != null && _gen.getSelectLoopDepth() == 0)
        out.println(loopVar + ":");
      
      out.println("while (" + iterName + ".hasNext()) {");
      out.pushDepth();
      _gen.pushSelectDepth();
      _gen.pushSelectLoopDepth();
      out.println("Node " + ptrName + " = (Node) " + iterName + ".next();");

      return ptrName;
    }

    if (select instanceof FromChildren) {
      name = printSelectBegin(out, select.getParent(), isForEach, loopVar);
      
      String ptrName = "_xsl_ptr" + _gen.generateId();

      if (loopVar != null && _gen.getSelectLoopDepth() == 0)
        out.println(loopVar + ":");
      
      out.println("for (Node " + ptrName + " = " + name + ".getFirstChild();");
      out.println("     " + ptrName + " != null;");
      out.println("     " + ptrName + " = " + ptrName + ".getNextSibling()) {");
      out.pushDepth();
      _gen.pushSelectDepth();
      _gen.pushSelectLoopDepth();

      return ptrName;
    }
    else if (select instanceof FromNextSibling) {
      name = printSelectBegin(out, select.getParent(), isForEach, loopVar);
      
      String ptrName = "_xsl_ptr" + _gen.generateId();
      
      if (loopVar != null && _gen.getSelectLoopDepth() == 0)
        out.println(loopVar + ":");
      
      out.println("for (Node " + ptrName + " = " + name + ".getNextSibling();");
      out.println("     " + ptrName + " != null;");
      out.println("     " + ptrName + " = " + ptrName + ".getNextSibling()) {");
      out.pushDepth();
      _gen.pushSelectDepth();
      _gen.pushSelectLoopDepth();

      return ptrName;
    }
    else if (select instanceof NodePattern) {
      name = printSelectBegin(out, select.getParent(), isForEach, loopVar);
      
      NodePattern pat = (NodePattern) select;
      
      out.println("if (" + name + ".getNodeName().equals(\"" + pat.getNodeName() + "\") &&");
      out.println("    " + name + " instanceof Element) {");
      out.pushDepth();
      _gen.pushSelectDepth();

      return name;
    }
    else if (select instanceof NodeTypePattern) {
      name = printSelectBegin(out, select.getParent(), isForEach, loopVar);
      
      NodeTypePattern pat = (NodeTypePattern) select;

      if (pat.getNodeType() >= 0) {
        out.println("if (" + name + ".getNodeType() == " + pat.getNodeType() + ") {");
        out.pushDepth();
        _gen.pushSelectDepth();
      }

      return name;
    }
    else if (select instanceof FilterPattern) {
      String posId = "_xsl_pos" + _gen.generateId();

      out.println("int " + posId + " = 0;");
      
      name = printSelectBegin(out, select.getParent(), isForEach, loopVar);

      out.println(posId + "++;");
      
      FilterPattern pat = (FilterPattern) select;
      Expr expr = pat.getExpr();

      if (expr instanceof NumericExpr) {
        NumericExpr num = (NumericExpr) expr;
        if (num.isConstant()) {
          out.println("if (" + posId + " > " + (int) num.getValue() + ")");
          out.println("  break;");
          out.println("else if (" + posId + " == " + (int) num.getValue() + ") {");
          out.pushDepth();
          _gen.pushSelectDepth();

          return name;
        }
      }

      throw new RuntimeException();
    }

    throw new RuntimeException(String.valueOf(select));
  }

  /**
   * Returns true if we can compile in the java select.
   */
  protected boolean allowJavaSelect(AbstractPattern select)
  {
    if (select == null)
      return false;

    else if (! select.isStrictlyAscending())
      return false;
    
    else if (select instanceof FromContext)
      return ((FromContext) select).getCount() == 0;

    else if (select instanceof FromRoot)
      return true;

    else if (select instanceof NodePattern)
      return allowJavaSelect(select.getParent());

    else if (select instanceof NodeTypePattern)
      return allowJavaSelect(select.getParent());

    else if (select instanceof FromChildren)
      return allowJavaSelect(select.getParent());

    else if (select instanceof FromNextSibling)
      return allowJavaSelect(select.getParent());

    else if (select instanceof FilterPattern) {
      if (! allowJavaSelect(select.getParent()))
        return false;

      Expr expr = ((FilterPattern) select).getExpr();

      return ((expr instanceof NumericExpr) &&
              ((NumericExpr) expr).isConstant());
    }

    else
      return false;
  }

  protected void printNamespace(JavaWriter out, NamespaceContext namespace)
    throws Exception
  {
    int index = _gen.addNamespace(namespace);

    out.print("_namespaces[" + index + "]");
  }

  /**
   * Prints the children as a fragment stored in a variable.
   */
  protected void printFragmentString(JavaWriter out, String id)
    throws Exception
  {
    String fragId = "_frag_" + _gen.generateId();
    
    out.println("XMLWriter " + fragId + " = out.pushFragment();");

    generateChildren(out);

    out.println(id + " = com.caucho.xml.XmlUtil.textValue(out.popFragment(" + fragId + "));");
  }

  /**
   * Prints the children as a fragment stored in a variable.
   */
  protected void printFragmentValue(JavaWriter out, String id)
    throws Exception
  {
    String fragId = "_frag_" + _gen.generateId();
    
    out.println("XMLWriter " + fragId + " = out.pushFragment();");

    generateChildren(out);

    out.println(id + " = out.popFragment(" + fragId + ");");
  }

  protected void popScope(JavaWriter out)
    throws Exception
  {
    printPopScope(out);
  }

  protected void printPopScope(JavaWriter out)
    throws Exception
  {
    if (_varCount > 0)
      out.println("env.popVars(" + _varCount + ");");
  }

  /**
   * Prints a test expr.
   */
  protected void printExprTest(JavaWriter out, int id, String node)
    throws IOException
  {
    out.print("_exprs[" + id + "].evalBoolean(" + node + 
	      ", " + getEnv() + ")");
  }

  public AbstractPattern parseMatch(String pattern)
    throws XslParseException, IOException
  {
    try {
      return XPath.parseMatch(pattern, getMatchNamespace()).getPattern();
    } catch (Exception e) {
      throw error(L.l("{0} in pattern `{1}'",
                      e.toString(), pattern));
    }
  }

  protected AbstractPattern parseSelect(String pattern)
    throws XslParseException
  {
    try {
      return XPath.parseSelect(pattern, getMatchNamespace()).getPattern();
    } catch (Exception e) {
      throw error(e);
    }
  }

  protected int addExpr(String pattern)
    throws XslParseException
  {
    return _gen.addExpr(parseExpr(pattern));
  }

  /**
   * Parses an XPath expression in the current context.
   */
  protected Expr parseExpr(String pattern)
    throws XslParseException
  {
    try {
      return XPath.parseExpr(pattern,
			     getMatchNamespace(),
			     _gen.getNodeListContext());
    } catch (Exception e) {
      throw error(e);
    }
  }

  protected int generateId()
  {
    return _gen.generateId();
  }

  protected String getEnv()
  {
    return _gen.getEnv();
  }

  public String escapeJavaString(String s)
  {
    if (s == null)
      return "";

    CharBuffer cb = CharBuffer.allocate();
    for (int i = 0; i < s.length(); i++) {
      if (s.charAt(i) == '\\')
        cb.append("\\\\");
      else if (s.charAt(i) == '"')
        cb.append("\\\"");
      else if (s.charAt(i) == '\n')
        cb.append("\\n");
      else if (s.charAt(i) == '\r')
        cb.append("\\r");
      else
        cb.append(s.charAt(i));
    }

    return cb.close();
  }

  /**
   * Creates a parse exception with the proper line information.
   */
  protected XslParseException error(String msg)
  {
    String filename = _filename;
    if (filename == null)
      filename = _systemId;
    
    if (filename != null)
      return new XslParseException(filename + ":" + _startLine + ": " + msg);
    else
      return new XslParseException(msg);
  }

  /**
   * Creates a parse exception with the proper line information.
   */
  protected XslParseException error(Throwable e)
  {
    String filename = _filename;
    if (filename == null)
      filename = _systemId;
    
    if (filename == null || e instanceof LineCompileException)
      return new XslParseException(e);
    else if (e instanceof CompileException)
      return new XslParseException(filename + ":" + _startLine + ": " +
                                   e.getMessage(), e);
    else
      return new XslParseException(_filename + ":" + _startLine + ": " +
                                   String.valueOf(e), e);
  }

  /**
   * Returns a printable version of the node.
   */
  public String toString()
  {
    if (_name == null)
      return "<" + getClass().getName() + ">";
    else
      return "<" + _name.getName() + ">";
  }
}
