/* * Copyright (c) 1998-2011 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 Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.xsl; import com.caucho.java.LineMap; import com.caucho.util.CharBuffer; import com.caucho.util.IntArray; import com.caucho.vfs.Path; import com.caucho.xml.CauchoNode; import com.caucho.xml.QAbstractNode; import com.caucho.xml.QElement; import com.caucho.xml.XMLWriter; import com.caucho.xml.XmlChar; import com.caucho.xml.XmlUtil; import com.caucho.xpath.Env; import com.caucho.xpath.Expr; import com.caucho.xpath.StylesheetEnv; import com.caucho.xpath.XPath; import com.caucho.xpath.XPathException; import com.caucho.xpath.XPathFun; import com.caucho.xpath.pattern.AbstractPattern; import com.caucho.xpath.pattern.NodeIterator; import com.caucho.xsl.fun.DocumentFun; import com.caucho.xsl.fun.ExtensionElementFun; import com.caucho.xsl.fun.ExtensionFunctionFun; import com.caucho.xsl.fun.SystemPropertyFun; import com.caucho.xsl.fun.UnparsedEntityFun; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import org.xml.sax.SAXException; import javax.xml.transform.TransformerException; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.logging.Logger; /** * Implementation base class for stylesheets. It is made public only * because generated Java and JavaScript classes need to access these * routines. */ public class StylesheetImpl extends AbstractStylesheet { private static final Logger log = Logger.getLogger(StylesheetImpl.class.getName()); public char []text; // static buffer of the text nodes protected HashMap templates; HashMap<String,XPathFun> _funs = new HashMap<String,XPathFun>(); private HashMap<String,String> _preserve; private HashMap<String,String> _strip; private HashMap<String,String> _preservePrefix; private HashMap<String,String> _stripPrefix; private HashMap<String,Object> _properties = new HashMap<String,Object>(); boolean isCacheable = true; protected boolean _defaultDisableEscaping; Path cachePath; long lastModified; boolean _generateLocation; LineMap lineMap; protected void copy(AbstractStylesheet stylesheet) { super.copy(stylesheet); StylesheetImpl stylesheetImpl = (StylesheetImpl) stylesheet; stylesheetImpl.text = text; stylesheetImpl.templates = templates; stylesheetImpl._preserve = _preserve; stylesheetImpl._strip = _strip; stylesheetImpl._preservePrefix = _preservePrefix; stylesheetImpl._stripPrefix = _stripPrefix; stylesheetImpl.lineMap = lineMap; stylesheetImpl._properties = _properties; stylesheetImpl._defaultDisableEscaping = _defaultDisableEscaping; } public OutputFormat getOutputFormat() { return new OutputFormat(); } public void setOutputFormat(OutputFormat output) { } protected void setSpaces(HashMap<String,String> preserve, HashMap<String,String> preservePrefix, HashMap<String,String> strip, HashMap<String,String> stripPrefix) { _preserve = preserve; _strip = strip; _preservePrefix = preservePrefix; _stripPrefix = stripPrefix; } public void setProperty(String name, Object value) { _properties.put(name, value); } public void setGenerateLocation(boolean generateLocation) { _generateLocation = generateLocation; } public boolean getGenerateLocation() { return _generateLocation; } public Object getProperty(String name) { Object value = _properties.get(name); if (value != null) return value; return super.getProperty(name); } protected void addFunction(String name, XPathFun fun) { _funs.put(name, fun); } public void init(Path path) throws Exception { super.init(path); addFunction("system-property", new SystemPropertyFun()); addFunction("element-available", new ExtensionElementFun()); addFunction("function-available", new ExtensionFunctionFun()); addFunction("unparsed-entity-uri", new UnparsedEntityFun()); } /** * Transforms the input node to the output writer * * @param xml the input node to be transformed * @param writer output writer receiving the output * @param transformer the transformer to be used */ public void transform(Node xml, XMLWriter writer, TransformerImpl transformer) throws SAXException, IOException, TransformerException { if (xml == null) throw new NullPointerException("can't transform null node"); XslWriter out = new XslWriter(null, this, transformer); out.init(writer); if (_funs == null) _funs = (HashMap) ((StylesheetImpl) _stylesheet)._funs.clone(); else _funs.putAll((HashMap) ((StylesheetImpl) _stylesheet)._funs); addFunction("document", new DocumentFun(transformer)); DocumentFun docFun = new DocumentFun(transformer); docFun.setHtml(true); addFunction("html_document", docFun); Env env = XPath.createEnv(); env.setFunctions(_funs); StylesheetEnv ssEnv = new StylesheetEnv(); ssEnv.setPath(getPath()); env.setStylesheetEnv(ssEnv); out.disableEscaping(_defaultDisableEscaping); if (_strip != null && ! _strip.isEmpty()) { stripSpaces(xml); } try { _xsl_init(out, xml, env); applyNode(out, xml, env, 0, Integer.MAX_VALUE); } catch (TransformerException e) { throw e; } catch (IOException e) { throw e; } catch (SAXException e) { throw e; } catch (Exception e) { throw new XslException(e, lineMap); } out.close(); XPath.freeEnv(env); // funs = null; } protected void _xsl_init(XslWriter out, Node context, Env env) throws Exception { } protected Document ownerDocument(Node node) { Document owner = node.getOwnerDocument(); if (owner != null) return owner; else return (Document) node; } public void applyNode(XslWriter out, Node node, Env env) throws Exception { applyNode(out, node, env, Integer.MIN_VALUE, Integer.MAX_VALUE); } protected void applyNode(XslWriter out, Node node, Env env, int min, int max) throws Exception { if (node == null) return; switch (node.getNodeType()) { case Node.DOCUMENT_NODE: case Node.DOCUMENT_FRAGMENT_NODE: for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { applyNode(out, child, env, 0, 2147483647); } break; case Node.ELEMENT_NODE: out.pushCopy(node); if (node instanceof QElement) { for (Node child = ((QElement) node).getFirstAttribute(); child != null; child = child.getNextSibling()) { applyNode(out, child, env, 0, 2147483647); } } else { NamedNodeMap attributeMap = ((Element) node).getAttributes(); int size = attributeMap.getLength(); for (int i = 0; i < size; i++) { Node child = attributeMap.item(i); applyNode(out, child, env, 0, 2147483647); } } for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { applyNode(out, child, env, 0, 2147483647); } out.popCopy(node); break; case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: String value = node.getNodeValue(); out.print(value); return; case Node.ATTRIBUTE_NODE: out.pushCopy(node); out.popCopy(node); break; case Node.ENTITY_REFERENCE_NODE: out.pushCopy(node); out.popCopy(node); break; } } /** * Gets a template. * * Only those templates with importance between min and max are considered. * For apply-templates, min = 0, and max = Infinity, * * @param min minimum allowed importance * @param max maximum allowed importance */ protected Template getTemplate(HashMap templates, Node node, Env env, int min, int max) throws XPathException { Template template = null; Template []templateList = (Template []) templates.get(node.getNodeName()); if (templateList == null) templateList = (Template []) templates.get("*"); for (int i = 0; templateList != null && i < templateList.length; i++) { Template subtemplate = templateList[i]; if (min <= subtemplate.maxImportance && subtemplate.maxImportance <= max && subtemplate.pattern.match(node, env)) { return subtemplate; } } return null; } /** * The default rule when no templates match. By default, it * calls apply-template on element children and copies text. All * other nodes are stripped. * * @param out the current writer. * @param node the current node. * @param env the xpath environment. */ protected void applyNodeDefault(XslWriter out, Node node, Env env) throws Exception { switch (node.getNodeType()) { case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: if (_generateLocation && node instanceof QAbstractNode) out.setLocation(((QAbstractNode) node).getBaseURI(), ((QAbstractNode) node).getFilename(), ((QAbstractNode) node).getLine()); String value = node.getNodeValue(); out.print(value); return; case Node.ATTRIBUTE_NODE: case Node.ENTITY_REFERENCE_NODE: out.print(node.getNodeValue()); break; case Node.ELEMENT_NODE: case Node.DOCUMENT_NODE: throw new RuntimeException(); } } public void printValue(XslWriter out, Node node) throws IOException { if (node != null) out.print(getNodeValue(node)); } public String getNodeValue(Node node) { CharBuffer cb = new CharBuffer(); nodeValue(cb, node); return cb.toString(); } private void nodeValue(CharBuffer cb, Node node) { if (node == null) return; switch (node.getNodeType()) { case Node.ELEMENT_NODE: for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { switch (child.getNodeType()) { case Node.ELEMENT_NODE: case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: case Node.ENTITY_REFERENCE_NODE: nodeValue(cb, child); break; } } break; case Node.ENTITY_REFERENCE_NODE: cb.append('&'); cb.append(node.getNodeName()); cb.append(';'); break; case Node.DOCUMENT_NODE: Document doc = (Document) node; nodeValue(cb, doc.getDocumentElement()); break; case Node.TEXT_NODE: case Node.CDATA_SECTION_NODE: String value = node.getNodeValue(); cb.append(value); break; default: cb.append(node.getNodeValue()); break; } } protected ArrayList xslSort(Node node, Env env, AbstractPattern pattern, Sort []sortList) throws Exception { ArrayList<Node> sortKeys = new ArrayList<Node>(); Iterator<Node> sortIter; NodeIterator iter = pattern.select(node, env); while (iter.hasNext()) { Node child = iter.next(); sortKeys.add(child); } int []map = new int[sortKeys.size()]; for (int i = map.length - 1; i >= 0; i--) map[i] = i; int []workMap = new int[map.length]; Object []values = new Object[map.length * sortList.length]; int size = map.length; for (int i = 0; i < size; i++) { Node child = (Node) sortKeys.get(i); env.setPosition(i + 1); // XXX: set last() as well for (int j = 0; j < sortList.length; j++) { Sort sort = sortList[j]; Object value = sort.sortValue(child, env); values[i * sortList.length + j] = value; } } boolean []ascendingList = new boolean[sortList.length]; for (int i = 0; i < ascendingList.length; i++) { Expr isAscending = sortList[i].getAscending(); if (isAscending == null || isAscending.evalBoolean(node, env)) ascendingList[i] = true; } Comparator []comparatorList = new Comparator[sortList.length]; for (int i = 0; i < comparatorList.length; i++) { Expr langExpr = sortList[i].getLang(); String lang = null; if (langExpr != null) { lang = langExpr.evalString(node, env); } if (lang != null) comparatorList[i] = getComparator(lang); } int []caseOrderList = new int[sortList.length]; for (int i = 0; i < caseOrderList.length; i++) { Expr caseOrder = sortList[i].getCaseOrder(); if (caseOrder == null) caseOrderList[i] = Sort.NO_CASE_ORDER; else if (caseOrder.evalBoolean(node, env)) caseOrderList[i] = Sort.UPPER_FIRST; else caseOrderList[i] = Sort.LOWER_FIRST; } sort(values, sortList, comparatorList, ascendingList, caseOrderList, 0, map.length, map, workMap); ArrayList sortedKeys = new ArrayList(); for (int i = 0; i < map.length; i++) sortedKeys.add(sortKeys.get(map[i])); return sortedKeys; } /** * Returns the comparator for the language. */ private Comparator getComparator(String lang) { Locale locale = getLocale(lang); return java.text.Collator.getInstance(locale); } /** * Returns the locale for the language. */ private Locale getLocale(String lang) { int p = lang.indexOf('-'); Locale locale = null; if (p < 0) { locale = new Locale(lang, ""); } else { String language = lang.substring(0, p); int q = lang.indexOf(p + 1, '-'); if (q < 0) { String country = lang.substring(p + 1); locale = new Locale(language, country); } else { String country = lang.substring(p + 1, q); String variant = lang.substring(q); locale = new Locale(language, country, variant); } } return locale; } /** * Sorts a subsequence. * * @param head the start of the subsequence * @param tail the tail of the subsequence */ private void sort(Object []values, Sort []sortList, Comparator []comparatorList, boolean []ascendingList, int []caseOrder, int head, int tail, int map[], int []workMap) { int length = tail - head; if (length <= 1) return; // shortcut when only have two items if (length == 2) { int a = map[head]; int b = map[head + 1]; if (lessThan(values, sortList, comparatorList, ascendingList, caseOrder, b, a)) { map[head] = b; map[head + 1] = a; } return; } // shortcut when only have three items else if (length == 3) { int a = map[head]; int b = map[head + 1]; int c = map[head + 2]; if (lessThan(values, sortList, comparatorList, ascendingList, caseOrder, b, a)) { map[head] = b; map[head + 1] = a; a = map[head]; b = map[head + 1]; } if (! lessThan(values, sortList, comparatorList, ascendingList, caseOrder, c, b)) { } else if (lessThan(values, sortList, comparatorList, ascendingList, caseOrder, c, a)) { map[head] = c; map[head + 1] = a; map[head + 2] = b; } else { map[head + 1] = c; map[head + 2] = b; } return; } int pivotIndex = (head + tail) / 2; int pivot = map[pivotIndex]; int top = tail; // values greater than the pivot value are put in the work map for (int i = tail - 1; i >= head; i--) { if (lessThan(values, sortList, comparatorList, ascendingList, caseOrder, pivot, map[i])) { workMap[--top] = map[i]; map[i] = -1; } } // if the pivot is the max, need to shift equals if (top == tail) { // values greater than the pivot value are put in the work map for (int i = tail - 1; i >= head; i--) { if (! lessThan(values, sortList, comparatorList, ascendingList, caseOrder, map[i], pivot)) { workMap[--top] = map[i]; map[i] = -1; } } // If all entries are equal to the pivot, we're done if (top == head) { for (int i = head; i < tail; i++) map[i] = workMap[i]; return; } } // shift down the values less than the pivot int center = head; for (int i = head; i < tail; i++) { if (map[i] >= 0) map[center++] = map[i]; } for (int i = center; i < tail; i++) map[i] = workMap[i]; sort(values, sortList, comparatorList, ascendingList, caseOrder, head, center, map, workMap); sort(values, sortList, comparatorList, ascendingList, caseOrder, center, tail, map, workMap); } /** * Swaps two items in the map. */ private void swap(int []map, int a, int b) { int ka = map[a]; int kb = map[b]; map[b] = ka; map[a] = kb; } /** * Returns true if the first value is strictly less than the second. */ private boolean lessThan(Object []values, Sort []sortList, Comparator []comparatorList, boolean []ascendingList, int []caseOrder, int ai, int bi) { int len = sortList.length; for (int i = 0; i < len; i++) { Object a = values[len * ai + i]; Object b = values[len * bi + i]; int cmp = sortList[i].cmp(a, b, comparatorList[i], ascendingList[i], caseOrder[i]); if (cmp < 0) return true; else if (cmp > 0) return false; } return false; } public void singleNumber(XslWriter out, Node node, Env env, AbstractPattern countPattern, AbstractPattern fromPattern, XslNumberFormat format) throws Exception { if (countPattern == null) countPattern = XPath.parseMatch(node.getNodeName()).getPattern(); IntArray numbers = new IntArray(); for (; node != null; node = node.getParentNode()) { if (countPattern.match(node, env)) { numbers.add(countPreviousSiblings(node, env, countPattern)); break; } if (fromPattern != null && fromPattern.match(node, env)) break; } if (fromPattern != null && ! findFromAncestor(node, env, fromPattern)) numbers.clear(); format.format(out, numbers); } public void multiNumber(XslWriter out, Node node, Env env, AbstractPattern countPattern, AbstractPattern fromPattern, XslNumberFormat format) throws Exception { if (countPattern == null) countPattern = XPath.parseMatch(node.getNodeName()).getPattern(); IntArray numbers = new IntArray(); for (; node != null; node = node.getParentNode()) { if (countPattern.match(node, env)) numbers.add(countPreviousSiblings(node, env, countPattern)); if (fromPattern != null && fromPattern.match(node, env)) break; } if (fromPattern != null && ! findFromAncestor(node, env, fromPattern)) numbers.clear(); format.format(out, numbers); } public void anyNumber(XslWriter out, Node node, Env env, AbstractPattern countPattern, AbstractPattern fromPattern, XslNumberFormat format) throws Exception { if (countPattern == null) countPattern = XPath.parseMatch(node.getNodeName()).getPattern(); IntArray numbers = new IntArray(); int count = 0; for (; node != null; node = XmlUtil.getPrevious(node)) { if (countPattern.match(node, env)) count++; if (fromPattern != null && fromPattern.match(node, env)) break; } numbers.add(count); if (fromPattern != null && ! findFromAncestor(node, env, fromPattern)) numbers.clear(); format.format(out, numbers); } public void exprNumber(XslWriter out, Node node, Env env, Expr expr, XslNumberFormat format) throws Exception { IntArray numbers = new IntArray(); numbers.add((int) expr.evalNumber(node, env)); format.format(out, numbers); } private int countPreviousSiblings(Node node, Env env, String name) { int count = 1; for (node = node.getPreviousSibling(); node != null; node = node.getPreviousSibling()) { if (node.getNodeType() == node.ELEMENT_NODE && node.getNodeName().equals(name)) count++; } return count; } private int countPreviousSiblings(Node node, Env env, AbstractPattern pattern) throws XPathException { int count = 1; for (node = node.getPreviousSibling(); node != null; node = node.getPreviousSibling()) { if (pattern.match(node, env)) count++; } return count; } private boolean findFromAncestor(Node node, Env env, AbstractPattern pattern) throws XPathException { for (; node != null; node = node.getParentNode()) if (pattern.match(node, env)) return true; return false; } /** * Strips the spaces from a tree. */ void stripSpaces(Node node) { Node child = node.getFirstChild(); while (child != null) { Node next = child.getNextSibling(); if (child instanceof Element) { stripSpaces(child); } else if (child instanceof Text) { String data = ((Text) child).getData(); boolean hasContent = false; for (int i = data.length() - 1; i >= 0; i--) { char ch = data.charAt(i); if (! XmlChar.isWhitespace(ch)) { hasContent = true; break; } } if (! hasContent && isStripSpaces(node)) { node.removeChild(child); } } child = next; } } /** * Returns true if the node is a pure whitespace text node. */ boolean isStripSpaces(Node node) { if (_strip == null) return false; for (Node ptr = node; ptr != null; ptr = ptr.getParentNode()) { if (ptr instanceof Element) { Element elt = (Element) ptr; String space = elt.getAttribute("xml:space"); if (space != null && space.equals("preserve")) return false; else if (space != null) break; } } String name = node.getNodeName(); if (_preserve.get(node.getNodeName()) != null) return false; else if (_strip.get(node.getNodeName()) != null) return true; CauchoNode cnode = (CauchoNode) node; String nsStar = cnode.getPrefix(); if (_preservePrefix.get(nsStar) != null) return false; else if (_stripPrefix.get(nsStar) != null) return true; return _strip.get("*") != null; } /** * Merges two template arrays into the final one. */ protected static Template []mergeTemplates(Template []star, Template []templates) { Template []merged = new Template[star.length + templates.length]; int i = 0; int j = 0; int k = 0; while (i < star.length && j < templates.length) { if (star[i].compareTo(templates[j]) > 0) merged[k++] = star[i++]; else merged[k++] = templates[j++]; } for (; i < star.length; i++) merged[k++] = star[i]; for (; j < templates.length; j++) merged[k++] = templates[j]; return merged; } }