Java tutorial
/** * Java Page Templates * Copyright (C) 2004 webslingerZ, inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.webslingerz.jpt; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Stack; import java.util.TreeMap; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import org.cyberneko.html.parsers.SAXParser; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Namespace; import org.dom4j.Node; import org.dom4j.io.SAXReader; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.AttributesImpl; import bsh.Interpreter; /** * @author <a href="mailto:rossi@webslingerZ.com">Chris Rossi</a> * @version $Revision: 1.15 $ */ public class PageTemplateImpl implements PageTemplate { /** * Strict mode requires the template to be well-formed XML * Non-strict mode allows use of tal: and metal: prefixes without having to * explicitly decleare XML namespaces for them, also uses NekoHTML parser * to parse HTML template and fix it to be proper XHTML document */ private boolean strict; private final String TAL_NAMESPACE_PREFIX = "tal"; private final String METAL_NAMESPACE_PREFIX = "metal"; private URI uri; private Document template; private Resolver userResolver = null; // Map of macros contained in this template Map<String, Macro> macros = new HashMap<String, Macro>(); private static SAXReader htmlReader = null; private static SAXReader xmlReader = null; private static SAXReader createXMLReader() throws SAXException { SAXReader reader = new SAXReader(); reader.setIgnoreComments(false); reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); return reader; } synchronized static final SAXReader getXMLReader() throws Exception { if (xmlReader == null) { xmlReader = createXMLReader(); } return xmlReader; } synchronized static final SAXReader getHTMLReader() throws Exception { if (htmlReader == null) { htmlReader = createXMLReader(); SAXParser parser = new SAXParser(); parser.setProperty("http://cyberneko.org/html/properties/names/elems", "match"); parser.setProperty("http://cyberneko.org/html/properties/names/attrs", "no-change"); parser.setProperty("http://cyberneko.org/html/properties/default-encoding", "UTF-8"); htmlReader.setXMLReader(parser); } return htmlReader; } public PageTemplateImpl() { this.uri = null; this.userResolver = null; this.strict = false; } public PageTemplateImpl(String template) throws PageTemplateException, UnsupportedEncodingException { this(); setTemplate(template); } public PageTemplateImpl(String template, Resolver resolver) throws PageTemplateException, UnsupportedEncodingException { this(); setResolver(resolver); setTemplate(template); } public PageTemplateImpl(InputStream input) throws PageTemplateException { this(); setTemplate(input); } public PageTemplateImpl(InputStream input, Resolver resolver) throws PageTemplateException { this(); setResolver(resolver); setTemplate(input); } public PageTemplateImpl(URL url) throws PageTemplateException { this(); setTemplate(url); } public void setTemplate(String template) throws UnsupportedEncodingException, PageTemplateException { setTemplate(new ByteArrayInputStream(template.getBytes("UTF-8"))); } public void setTemplate(InputStream input) throws PageTemplateException { readTemplate(input, null); } public void setTemplate(URL url) throws PageTemplateException { try { this.uri = new URI(url.toString()); } catch (URISyntaxException e) { throw new PageTemplateException(e); } readTemplate(null, url); } private void readTemplate(InputStream input, URL url) throws PageTemplateException { try { SAXReader reader = getXMLReader(); try { template = input != null ? reader.read(input) : reader.read(url); } catch (DocumentException e) { if (this.strict) { throw e; } try { reader = getHTMLReader(); template = input != null ? reader.read(input) : reader.read(url); } catch (NoClassDefFoundError ee) { /* Allow user to omit nekohtml package to disable html parsing */ throw e; } } } catch (Exception e) { throw new PageTemplateException(e); } } public Resolver getResolver() { return this.userResolver; } public void setResolver(Resolver resolver) { this.userResolver = resolver; } public void process(OutputStream output, Object context) throws SAXException, PageTemplateException, IOException { process(output, context, null); } static final SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); public void process(OutputStream output, Object context, Map<String, Object> dictionary) throws SAXException, PageTemplateException, IOException { try { TransformerHandler handler = factory.newTransformerHandler(); Transformer transformer = handler.getTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "html"); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); handler.setResult(new StreamResult(output)); process(handler, handler, context, dictionary, null); } catch (TransformerConfigurationException e) { throw new PageTemplateException(e); } } public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler, Object context, Map<String, Object> dictionary, Interpreter beanShell) throws SAXException, PageTemplateException, IOException { try { if (beanShell == null) { // Initialize the bean shell beanShell = new Interpreter(); if (dictionary != null) { for (Iterator i = dictionary.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry) i.next(); beanShell.set((String) entry.getKey(), entry.getValue()); } } beanShell.set("here", context); beanShell.set("template", this); beanShell.set("resolver", new DefaultResolver(context, dictionary, beanShell)); beanShell.set("bool", new BoolHelper()); beanShell.set("math", new MathHelper()); beanShell.set("date", new DateHelper()); } Element root = template.getRootElement(); contentHandler.startDocument(); processElement(root, contentHandler, lexicalHandler, beanShell, new Stack<Map<String, Slot>>()); contentHandler.endDocument(); } catch (bsh.EvalError e) { throw new PageTemplateException(e); } } private Map<String, String> namespaces = new TreeMap<String, String>(); private String getNamespaceURIFromPrefix(String prefix) { String uri = namespaces.get(prefix); return uri == null ? "" : uri; } private void processElement(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler, Interpreter beanShell, Stack<Map<String, Slot>> slotStack) throws SAXException, PageTemplateException, IOException { // Get attributes Expressions expressions = new Expressions(); AttributesImpl attributes = getAttributes(element, expressions); // Skip macro definitions if (expressions.macro) { return; } // Process instructions // 1. define if (expressions.define != null) { processDefine(expressions.define, beanShell); } // 2. condition if (expressions.condition != null && !Expression.evaluateBoolean(expressions.condition, beanShell)) { // Skip this element (and children) return; } // 3. repeat Loop loop = new Loop(expressions.repeat, beanShell); while (loop.repeat(beanShell)) { // expand macro if (expressions.useMacro != null) { processMacro(expressions.useMacro, element, contentHandler, lexicalHandler, beanShell, slotStack); continue; } // 4. content or replace Object jptContent = null; if (expressions.content != null) { jptContent = processContent(expressions.content, beanShell); } else // fill slots if (expressions.defineSlot != null) { // System.err.println( "fill slot: " + expressions.defineSlot ); if (!slotStack.isEmpty()) { Map<String, Slot> slots = slotStack.pop(); Slot slot = slots.get(expressions.defineSlot); // System.err.println( "slot: " + slot ); if (slot != null) { slot.process(contentHandler, lexicalHandler, beanShell, slotStack); slotStack.push(slots); return; } // else { use content in macro } slotStack.push(slots); } else { throw new PageTemplateException("slot definition not allowed outside of macro"); } } // 5. attributes if (expressions.attributes != null) { processAttributes(attributes, expressions.attributes, beanShell); } // 6. omit-tag boolean jptOmitTag = false; if (expressions.omitTag != null) { if (expressions.omitTag.equals("")) { jptOmitTag = true; } else { jptOmitTag = Expression.evaluateBoolean(expressions.omitTag, beanShell); } } // Output // Declare element Namespace namespace = element.getNamespace(); if (!jptOmitTag) { for (int i = 0; i < attributes.getLength(); i++) { String processedValue = Expression.evaluateText(attributes.getValue(i), beanShell); attributes.setValue(i, processedValue); } contentHandler.startElement(namespace.getURI(), element.getName(), element.getQualifiedName(), attributes); } // Process content if (jptContent != null) { // Content for this element has been generated dynamically if (jptContent instanceof HTMLFragment) { HTMLFragment html = (HTMLFragment) jptContent; html.toXhtml(contentHandler, lexicalHandler); } // plain text else { char[] text = ((String) jptContent).toCharArray(); contentHandler.characters(text, 0, text.length); } } else { defaultContent(element, contentHandler, lexicalHandler, beanShell, slotStack); } // End element if (!jptOmitTag) { contentHandler.endElement(namespace.getURI(), element.getName(), element.getQualifiedName()); } } } private void defaultContent(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler, Interpreter beanShell, Stack<Map<String, Slot>> slotStack) throws SAXException, PageTemplateException, IOException { // Use default template content for (Iterator i = element.nodeIterator(); i.hasNext();) { Node node = (Node) i.next(); switch (node.getNodeType()) { case Node.ELEMENT_NODE: processElement((Element) node, contentHandler, lexicalHandler, beanShell, slotStack); break; case Node.TEXT_NODE: char[] text = Expression.evaluateText(node.getText().toString(), beanShell).toCharArray(); contentHandler.characters(text, 0, text.length); break; case Node.COMMENT_NODE: char[] comment = node.getText().toCharArray(); lexicalHandler.comment(comment, 0, comment.length); break; case Node.CDATA_SECTION_NODE: lexicalHandler.startCDATA(); char[] cdata = node.getText().toCharArray(); contentHandler.characters(cdata, 0, cdata.length); lexicalHandler.endCDATA(); break; case Node.NAMESPACE_NODE: Namespace declared = (Namespace) node; // System.err.println( "Declared namespace: " + // declared.getPrefix() + ":" + declared.getURI() ); namespaces.put(declared.getPrefix(), declared.getURI()); // if ( declared.getURI().equals( TAL_NAMESPACE_URI ) ) { // this.talNamespacePrefix = declared.getPrefix(); // } // else if (declared.getURI().equals( METAL_NAMESPACE_URI ) ) { // this.metalNamespacePrefix = declared.getPrefix(); // } break; case Node.ATTRIBUTE_NODE: // Already handled break; case Node.DOCUMENT_TYPE_NODE: case Node.ENTITY_REFERENCE_NODE: case Node.PROCESSING_INSTRUCTION_NODE: default: // System.err.println( "WARNING: Node type not supported: " + // node.getNodeTypeName() ); } } } AttributesImpl getAttributes(Element element, Expressions expressions) throws PageTemplateException { AttributesImpl attributes = new AttributesImpl(); for (Iterator i = element.attributeIterator(); i.hasNext();) { Attribute attribute = (Attribute) i.next(); Namespace namespace = attribute.getNamespace(); Namespace elementNamespace = element.getNamespace(); if (!namespace.hasContent() && elementNamespace.hasContent()) namespace = elementNamespace; // String prefix = namespace.getPrefix(); // System.err.println( "attribute: name=" + attribute.getName() + // "\t" + // "qualified name=" + attribute.getQualifiedName() + "\t" + // "ns prefix=" + namespace.getPrefix() + "\t" + // "ns uri=" + namespace.getURI() ); // String qualifiedName = attribute.getName(); // String name = qualifiedName; // if ( qualifiedName.startsWith( prefix + ":" ) ) { // name = qualifiedName.substring( prefix.length() + 1 ); // } String name = attribute.getName(); // Handle JPT attributes // if ( prefix.equals( talNamespacePrefix ) ) { if (TAL_NAMESPACE_URI.equals(namespace.getURI()) || (!strict && TAL_NAMESPACE_PREFIX.equals(namespace.getPrefix()))) { // tal:define if (name.equals("define")) { expressions.define = attribute.getValue(); } // tal:condition else if (name.equals("condition")) { expressions.condition = attribute.getValue(); } // tal:repeat else if (name.equals("repeat")) { expressions.repeat = attribute.getValue(); } // tal:content else if (name.equals("content")) { expressions.content = attribute.getValue(); } // tal:replace else if (name.equals("replace")) { if (expressions.omitTag == null) { expressions.omitTag = ""; } expressions.content = attribute.getValue(); } // tal:attributes else if (name.equals("attributes")) { expressions.attributes = attribute.getValue(); } // tal:omit-tag else if (name.equals("omit-tag")) { expressions.omitTag = attribute.getValue(); } // error else { throw new PageTemplateException("unknown tal attribute: " + name); } } // else if ( prefix.equals( metalNamespacePrefix ) ) else if (METAL_NAMESPACE_URI.equals(namespace.getURI()) || (!strict && METAL_NAMESPACE_PREFIX.equals(namespace.getPrefix()))) { // metal:use-macro if (name.equals("use-macro")) { expressions.useMacro = attribute.getValue(); } // metal:define-slot else if (name.equals("define-slot")) { expressions.defineSlot = attribute.getValue(); } // metal:define-macro else if (name.equals("define-macro")) { //System.out.println("Defining macro: " + attribute.getValue()); Element el = element.createCopy(); el.remove(attribute); macros.put(attribute.getValue(), new MacroImpl(el)); expressions.macro = true; } // metal:fill-slot else if (name.equals("fill-slot")) { // these are ignored here, as they don't affect processing // of current template, but are called from other templates } // error else { throw new PageTemplateException("unknown metal attribute: " + name); } } // Pass on all other attributes else { String nsURI = namespace.getURI(); // String qualifiedName = namespace.getPrefix() + ":" + name; attributes.addAttribute(nsURI, name, attribute.getQualifiedName(), "CDATA", attribute.getValue()); if (nsURI != "" && namespace != elementNamespace) { String prefix = namespace.getPrefix(); String qName = "xmlns:" + prefix; if (attributes.getIndex(qName) == -1) { // add xmlns for this attribute attributes.addAttribute("", prefix, qName, "CDATA", nsURI); } } // attributes.addAttribute( getNamespaceURIFromPrefix(prefix), // name, qualifiedName, "CDATA", attribute.getValue() ); } } return attributes; } private Object processContent(String expression, Interpreter beanShell) throws PageTemplateException { // Structured text, preserve xml structure if (expression.startsWith("structure ")) { expression = expression.substring("structure ".length()); Object content = Expression.evaluate(expression, beanShell); if (!(content instanceof HTMLFragment)) { content = new HTMLFragment(String.valueOf(content)); } return content; } else if (expression.startsWith("text ")) { expression = expression.substring("text ".length()); } return String.valueOf(Expression.evaluate(expression, beanShell)); } private void processDefine(String expression, Interpreter beanShell) throws PageTemplateException { try { ExpressionTokenizer tokens = new ExpressionTokenizer(expression, ';', true); while (tokens.hasMoreTokens()) { String variable = tokens.nextToken().trim(); int space = variable.indexOf(' '); if (space == -1) { throw new ExpressionSyntaxException("bad variable definition: " + variable); } String name = variable.substring(0, space); String valueExpression = variable.substring(space + 1).trim(); Object value = Expression.evaluate(valueExpression, beanShell); beanShell.set(name, value); } } catch (bsh.EvalError e) { throw new PageTemplateException(e); } } private void processAttributes(AttributesImpl attributes, String expression, Interpreter beanShell) throws PageTemplateException { ExpressionTokenizer tokens = new ExpressionTokenizer(expression, ';', true); while (tokens.hasMoreTokens()) { String attribute = tokens.nextToken().trim(); int space = attribute.indexOf(' '); if (space == -1) { throw new ExpressionSyntaxException("bad attributes expression: " + attribute); } String qualifiedName = attribute.substring(0, space); String valueExpression = attribute.substring(space + 1).trim(); Object value = Expression.evaluate(valueExpression, beanShell); // System.err.println( "attribute:\n" + qualifiedName + "\n" + // valueExpression + "\n" + value ); removeAttribute(attributes, qualifiedName); if (value != null) { String name = ""; String uri = ""; int colon = qualifiedName.indexOf(":"); if (colon != -1) { String prefix = qualifiedName.substring(0, colon); name = qualifiedName.substring(colon + 1); uri = getNamespaceURIFromPrefix(prefix); } attributes.addAttribute(uri, name, qualifiedName, "CDATA", String.valueOf(value)); } } } private void removeAttribute(AttributesImpl attributes, String qualifiedName) { int index = attributes.getIndex(qualifiedName); if (index != -1) { attributes.removeAttribute(index); } } private void processMacro(String expression, Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler, Interpreter beanShell, Stack<Map<String, Slot>> slotStack) throws PageTemplateException { Object object; object = Expression.evaluate(expression, beanShell); if (object == null) { object = macros.get(expression); } if (object == null) { throw new NoSuchPathException("could not find macro: " + expression); } if (object instanceof Macro) { // Find slots to fill inside this macro call Map<String, Slot> slots = new HashMap<String, Slot>(); findSlots(element, slots); // Slots filled in later templates (processed earlier) // Take precedence over slots filled in intermediate // templates. if (!slotStack.isEmpty()) { Map<String, Slot> laterSlots = slotStack.peek(); slots.putAll(laterSlots); } slotStack.push(slots); // System.err.println( "found slots: " + slots.keySet() ); // Call macro Macro macro = (Macro) object; try { macro.process(contentHandler, lexicalHandler, beanShell, slotStack); } catch (Exception e) { throw new PageTemplateException(e); } } else { throw new PageTemplateException( "expression '" + expression + "' does not evaluate to macro: " + object.getClass().getName()); } } public Map<String, Macro> getMacros() { return this.macros; } /** * With all of our namespace woes, getting an XPath expression to work has * proven futile, so we'll recurse through the tree ourselves to find what * we need. */ private void findSlots(Element element, Map<String, Slot> slots) { // System.err.println( "element: " + element.getName() ); for (Iterator i = element.attributes().iterator(); i.hasNext();) { Attribute attribute = (Attribute) i.next(); // System.err.println( "\t" + attribute.getName() + "\t" + // attribute.getQualifiedName() ); } // Look for our attribute // String qualifiedAttributeName = this.metalNamespacePrefix + // ":fill-slot"; // String name = element.attributeValue( qualifiedAttributeName ); String name = element.attributeValue("fill-slot"); if (name != null) { slots.put(name, new SlotImpl(element)); } // Recurse into child elements for (Iterator i = element.elementIterator(); i.hasNext();) { findSlots((Element) i.next(), slots); } } public String toLetter(int n) { return Loop.formatLetter(n); } public String toCapitalLetter(int n) { return Loop.formatCapitalLetter(n); } public String toRoman(int n) { return Loop.formatRoman(n); } public String toCapitalRoman(int n) { return Loop.formatCapitalRoman(n); } public boolean isStrict() { return strict; } public void setStrict(boolean optionStrict) { this.strict = optionStrict; } class DefaultResolver extends Resolver { URIResolver uriResolver; private Object context; private Map<String, Object> dictionary; private Interpreter beanShell; DefaultResolver(Object context, Map<String, Object> dictionary, Interpreter beanShell) { this.context = context; this.dictionary = dictionary; this.beanShell = beanShell; if (uri != null) { uriResolver = new URIResolver(uri); } } public URL getResource(String path) throws java.net.MalformedURLException { URL resource = null; // If user has supplied resolver, use it if (userResolver != null) { resource = userResolver.getResource(path); } // If resource not found by user resolver // fall back to resolving by uri if (resource == null && uriResolver != null) { resource = uriResolver.getResource(path); } return resource; } public PageTemplate getPageTemplate(String path) throws PageTemplateException, java.net.MalformedURLException { PageTemplate template = null; // If user has supplied resolver, use it if (userResolver != null) { template = userResolver.getPageTemplate(path); // template inherits user resolver template.setResolver(userResolver); } // If template not found by user resolver // fall back to resolving by uri if (template == null && uriResolver != null) { template = uriResolver.getPageTemplate(path); } if (template != null) { try { TransformerHandler handler = factory.newTransformerHandler(); // use null result handler handler.setResult(new StreamResult(new OutputStream() { @Override public void write(int b) throws IOException { } })); template.process(handler, handler, context, dictionary, beanShell); } catch (Exception e) { throw new PageTemplateException(e); } } return template; } } class MacroImpl implements Macro { Element element; MacroImpl(Element element) { this.element = element; } public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler, Interpreter beanShell, Stack<Map<String, Slot>> slotStack) throws SAXException, PageTemplateException, IOException { processElement(element, contentHandler, lexicalHandler, beanShell, slotStack); } } class SlotImpl implements Slot { Element element; SlotImpl(Element element) { this.element = element; } public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler, Interpreter beanShell, Stack<Map<String, Slot>> slotStack) throws SAXException, PageTemplateException, IOException { processElement(element, contentHandler, lexicalHandler, beanShell, slotStack); } } } class Expressions { String define = null; String condition = null; String repeat = null; String content = null; String attributes = null; String omitTag = null; String useMacro = null; String defineSlot = null; boolean macro = false; } class BitBucket extends OutputStream { static java.io.PrintWriter getBitBucketPrintWriter() { return new java.io.PrintWriter(new java.io.OutputStreamWriter(new BitBucket())); } public void write(int b) { // off you go to bit heaven } } class BoolHelper { public static boolean and(boolean a, boolean b) { return a && b; } public static boolean or(boolean a, boolean b) { return a || b; } public static Object cond(boolean b, Object x, Object y) { return b ? x : y; } } class MathHelper { public final static int add(int x, int y) { return x + y; } public final static int sub(int x, int y) { return x - y; } public final static int mult(int x, int y) { return x * y; } public final static int div(int x, int y) { return x / y; } public final static int mod(int x, int y) { return x % y; } } class DateHelper { static final Map<String, SimpleDateFormat> dateFormats = new TreeMap<String, SimpleDateFormat>(); public static final String format(String format, Locale locale, Date date) { SimpleDateFormat dateFormat = dateFormats.get(format); if (dateFormat == null) { dateFormat = new SimpleDateFormat(format, locale); dateFormats.put(format, dateFormat); } return dateFormat.format(date); } }