/* Parser.java Purpose: Description: History: Sat Sep 17 12:12:41 2005, Created by tomyeh Copyright (C) 2004 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.web.servlet.dsp.impl; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.zkoss.idom.Element; import org.zkoss.idom.input.SAXBuilder; import org.zkoss.idom.util.IDOMs; import org.zkoss.lang.Classes; import org.zkoss.util.resource.Locator; import org.zkoss.web.mesg.MWeb; import org.zkoss.web.servlet.dsp.DspException; import org.zkoss.web.servlet.dsp.Interpretation; import org.zkoss.web.servlet.dsp.action.Action; import org.zkoss.web.servlet.dsp.action.Page; import org.zkoss.xel.ExpressionFactory; import org.zkoss.xel.Expressions; import org.zkoss.xel.FunctionMapper; import org.zkoss.xel.VariableResolver; import org.zkoss.xel.XelContext; import org.zkoss.xel.XelException; import org.zkoss.xel.taglib.Taglibs; import org.zkoss.xel.util.SimpleMapper; /** * Used to parse a DSP page into a meta format called * {@link Interpretation}. * * @author tomyeh */ public class Parser { /** Parses the content into a meta format * * @param content the content to parse; never null. * @param ctype the content type. Optional. It is used only if * no page action at all. If it is not specified and not page * action, "text/html" is assumed. * @param xelc the context information used to parse XEL expressions * in the content. * @param loc used to locate the resource such as taglib. * It could null only if DSP contains no such resource. */ public Interpretation parse(String content, String ctype, XelContext xelc, Locator loc) throws DspException, IOException, XelException { final Context ctx = new Context(content, xelc, loc); final RootNode root = new RootNode(); parse0(ctx, root, 0, content.length()); root.setFunctionMapper(ctx.getFunctionMapper()); if (!ctx.pageDefined) { //We always create a page definition final ActionNode action = new ActionNode(Page.class, 0); root.addChild(0, action); final Map<String, String> attrs = new HashMap<String, String>(2); if (ctype == null) ctype = "text/html"; else if (ctype.length() > 0 && ctype.charAt(0) == ';') ctype = "text/html" + ctype; attrs.put("optionalContentType", ctype); applyAttrs("page", action, attrs, ctx); } return root; } /** Recursively parse the content into a tree of {@link Node}. */ private static void parse0(Context ctx, Node parent, int from, int to) throws DspException, IOException, XelException { boolean esc = false; final StringBuffer sb = new StringBuffer(512); for (int j = from; j < to; ++j) { char cc = ctx.content.charAt(j); //We only recognize <%, <\%, ${, $\{ and <xx:yy> switch (cc) { case '<': if (j + 1 < to) { char c2 = ctx.content.charAt(j + 1); if (c2 == '\\') { if (j + 2 < to && ctx.content.charAt(j + 2) == '%') ++j; //skip '\\' } else if (c2 == '%') { addText(parent, sb); j = parseControl(ctx, parent, j, to); continue; } else { final int oldLines = ctx.nLines; int k = skipWhitespaces(ctx, j + 1, to); int l = nextSeparator(ctx, k, to); if (l >= to || l == k || ctx.content.charAt(l) != ':') { ctx.nLines = oldLines; break; //bypass what we don't recognize } final String prefix = ctx.content.substring(k, l); if (!ctx.hasPrefix(prefix)) { ctx.nLines = oldLines; break; //bypass what we don't recognize } addText(parent, sb); j = parseAction(ctx, parent, prefix, l, to); continue; } } break; case '$': if (j + 1 < to) { char c2 = ctx.content.charAt(j + 1); if (c2 == '\\') { if (j + 2 < to && ctx.content.charAt(j + 2) == '{') ++j; //skip '\\' } else if (c2 == '{') { addText(parent, sb); j = parseEL(ctx, parent, j, to); continue; } } break; case '\n': ++ctx.nLines; } sb.append(cc); } addText(parent, sb); } /** Parses a control (e.g., <% page %>) starting at from, * and returns the position of '>' (in %>). */ private static int parseControl(Context ctx, Node parent, int from, int to) throws DspException, IOException, XelException { int j = from + 2; if (j + 1 >= to) throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { null, new Integer(ctx.nLines) }); //0. comment char cc = ctx.content.charAt(j); if (cc == '-' && ctx.content.charAt(j + 1) == '-') { //comment for (int end = to - 4;; ++j) { if (j > end) throw new DspException(MWeb.DSP_COMMENT_NOT_TERMINATED, new Integer(ctx.nLines)); if (ctx.content.charAt(j) == '\n') ++ctx.nLines; else if (startsWith(ctx.content, j, to, "--%>")) return j + 3; } } if (cc != '@') throw new DspException(MWeb.DSP_EXPECT_CHARACTER, new Object[] { new Character('@'), new Integer(ctx.nLines) }); //1: which control j = skipWhitespaces(ctx, j + 1, to); int k = nextSeparator(ctx, j, to); if (k >= to) throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { null, new Integer(ctx.nLines) }); final ActionNode action; final String ctlnm = ctx.content.substring(j, k); if ("taglib".equals(ctlnm)) { action = null; } else if ("page".equals(ctlnm)) { ctx.pageDefined = true; trim(parent); //Bug 1798123: avoid getOut being called before Page parent.addChild(action = new ActionNode(Page.class, ctx.nLines)); } else { throw new DspException(MWeb.DSP_UNKNOWN_ACTION, new Object[] { ctlnm, new Integer(ctx.nLines) }); } //2: parse attributes final Map<String, String> attrs = new HashMap<String, String>(); k = parseAttrs(ctx, attrs, ctlnm, k, to); cc = ctx.content.charAt(k); if (cc != '%') throw new DspException(MWeb.DSP_EXPECT_CHARACTER, new Object[] { new Character('%'), new Integer(ctx.nLines) }); if (action == null) { //taglib final String uri = attrs.get("uri"), prefix = attrs.get("prefix"); if (prefix == null || uri == null) throw new DspException(MWeb.DSP_TAGLIB_ATTRIBUTE_REQUIRED, new Integer(ctx.nLines)); ctx.loadTaglib(prefix, uri); } else { applyAttrs(ctlnm, action, attrs, ctx); } if (++k >= to || ctx.content.charAt(k) != '>') throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { ctlnm, new Integer(ctx.nLines) }); return k; } /** Trimmed {@link TextNode} that contains nothing but spaces. */ private static void trim(Node node) { for (Iterator it = node.getChildren().iterator(); it.hasNext();) { final Object o = it.next(); if (o instanceof TextNode) { final String s = ((TextNode) o).getText(); if (s == null || s.trim().length() == 0) it.remove(); } } } /** Parses an action (e.g., <c:forEach...>...</c:forEach>). * @param from the position of ':' * @return the position of the last '>'. */ private static int parseAction(Context ctx, Node parent, String prefix, int from, int to) throws DspException, IOException, XelException { //1: which action int j = skipWhitespaces(ctx, from + 1, to); int k = nextSeparator(ctx, j, to); if (k >= to) throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { prefix + ':', new Integer(ctx.nLines) }); if (k == j) throw new DspException(MWeb.DSP_ACTION_REQUIRED, new Integer(ctx.nLines)); final String actnm = ctx.content.substring(j, k); final Class actcls = ctx.getActionClass(prefix, actnm); if (actcls == null) throw new DspException(MWeb.DSP_UNKNOWN_ACTION, new Object[] { prefix + ':' + actnm, new Integer(ctx.nLines) }); final ActionNode action = new ActionNode(actcls, ctx.nLines); parent.addChild(action); //2: action's attributes final Map<String, String> attrs = new HashMap<String, String>(); j = parseAttrs(ctx, attrs, actnm, k, to); char cc = ctx.content.charAt(j); boolean ended = cc == '/'; if (!ended && cc != '>') throw new DspException(MWeb.DSP_UNEXPECT_CHARACTER, new Object[] { new Character(cc), new Integer(ctx.nLines) }); applyAttrs(actnm, action, attrs, ctx); if (ended) { if (j + 1 >= to || ctx.content.charAt(j + 1) != '>') throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { prefix + ':' + actnm, new Integer(action.getLineNumber()) }); return j + 1; } //3: nested content final int nestedFrom = ++j, nestedTo; for (int depth = 0;; ++j) { if (j >= to) throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { actnm, new Integer(action.getLineNumber()) }); cc = ctx.content.charAt(j); if (j + 1 < to) { if (cc == '<') { final int oldLines = ctx.nLines; k = j + 1; ended = ctx.content.charAt(k) == '/'; k = skipWhitespaces(ctx, ended ? k + 1 : k, to); int l = nextSeparator(ctx, k, to); if (l >= to || ctx.content.charAt(l) != ':' || !prefix.equals(ctx.content.substring(k, l))) { ctx.nLines = oldLines; continue; //bypass } k = skipWhitespaces(ctx, l + 1, to); l = nextSeparator(ctx, k, to); if (l >= to || !actnm.equals(ctx.content.substring(k, l))) { ctx.nLines = oldLines; continue; //bypass } l = skipWhitespaces(ctx, l, to); if (l >= to || (ended && ctx.content.charAt(l) != '>')) { ctx.nLines = oldLines; continue; //bypass } if (ended) { if (--depth < 0) { nestedTo = j; j = l; break; //done } } else { ++depth; } j = l; continue; } else if (cc == '$' && ctx.content.charAt(j + 1) == '{') { j = endOfEL(ctx, j, to); continue; } } if (cc == '\n') ++ctx.nLines; } parse0(ctx, action, nestedFrom, nestedTo); //recursive return j; } private static boolean startsWith(String content, int from, int to, String s) { for (int j = 0, len = s.length();; ++from, ++j) { if (j >= len) return true; if (from >= to || content.charAt(from) != s.charAt(j)) return false; } } private static int skipWhitespaces(Context ctx, int from, int to) { for (; from < to; ++from) { final char cc = ctx.content.charAt(from); if (cc == '\n') ++ctx.nLines; else if (!Character.isWhitespace(cc)) break; } return from; } private static int nextSeparator(Context ctx, int from, int to) { for (; from < to; ++from) { final char cc = ctx.content.charAt(from); if ((cc < '0' || cc > '9') && (cc < 'a' || cc > 'z') && (cc < 'A' || cc > 'Z') && cc != '_') break; } return from; } /** Parses the attributes. */ private static int parseAttrs(Context ctx, Map<String, String> attrs, String actnm, int from, int to) throws DspException { for (int j, k = from;;) { j = skipWhitespaces(ctx, k, to); k = nextSeparator(ctx, j, to); if (k >= to) throw new DspException(MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] { actnm, new Integer(ctx.nLines) }); if (j == k) return j; final String attrnm = ctx.content.substring(j, k); k = skipWhitespaces(ctx, k, to); j = skipWhitespaces(ctx, k + 1, to); if (j >= to || ctx.content.charAt(k) != '=') throw new DspException(MWeb.DSP_ATTRIBUTE_VALUE_REQUIRED, new Object[] { actnm, attrnm, new Integer(ctx.nLines) }); final char quot = ctx.content.charAt(j); if (quot != '"' && quot != '\'') throw new DspException(MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED, new Object[] { actnm, attrnm, new Integer(ctx.nLines) }); final StringBuffer sbval = new StringBuffer(); for (k = ++j;; ++k) { if (k >= to) throw new DspException(MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED, new Object[] { actnm, attrnm, new Integer(ctx.nLines) }); final char cc = ctx.content.charAt(k); if (cc == '\n') throw new DspException(MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED, new Object[] { actnm, attrnm, new Integer(ctx.nLines) }); if (cc == quot) { ++k; break; //found } sbval.append(cc); if (cc == '\\' && ++k < to) sbval.setCharAt(sbval.length() - 1, ctx.content.charAt(k)); } attrs.put(attrnm, sbval.toString()); } } /** Applies attributes. */ private static final void applyAttrs(String actnm, ActionNode action, Map<String, String> attrs, ParseContext ctx) throws DspException, XelException { for (Map.Entry<String, String> me : attrs.entrySet()) { final String attrnm = me.getKey(); final String attrval = me.getValue(); try { action.addAttribute(attrnm, attrval, ctx); } catch (NoSuchMethodException ex) { throw new DspException(MWeb.DSP_ATTRIBUTE_NOT_FOUND, new Object[] { actnm, attrnm, new Integer(action.getLineNumber()) }); } catch (ClassCastException ex) { throw new DspException(MWeb.DSP_ATTRIBUTE_INVALID_VALUE, new Object[] { actnm, attrnm, attrval, new Integer(action.getLineNumber()) }, ex); } } } /** Parses an EL expression starting at from. * @return the position of }. */ private static int parseEL(Context ctx, Node parent, int from, int to) throws DspException, XelException { int j = endOfEL(ctx, from, to); //point to } parent.addChild(new XelNode(ctx.content.substring(from, j + 1), ctx)); return j; } /** Returns the position of '}'. */ private static int endOfEL(Context ctx, int from, int to) throws DspException { for (int j = from + 2;; ++j) { if (j >= to) throw new DspException(MWeb.EL_NOT_TERMINATED, new Integer(ctx.nLines)); final char cc = ctx.content.charAt(j); if (cc == '}') { return j; } else if (cc == '\'' || cc == '"') { while (++j < to) { final char c2 = ctx.content.charAt(j); if (c2 == cc) break; if (cc == '\n') throw new DspException("Illegal EL expression: non-terminaled " + cc + " at line " + ctx.nLines + " character " + j); if (c2 == '\\' && ++j < to && ctx.content.charAt(j) == '\n') ++ctx.nLines; } } else if (cc == '\n') { ++ctx.nLines; } } } /** Adds a text node. */ private static void addText(Node parent, StringBuffer sb) { if (sb.length() > 0) { parent.addChild(new TextNode(sb.toString())); sb.setLength(0); } } /** Context used for parsing. */ private static class Context implements ParseContext { private final String content; /** (String prefix, Map(String name, Class class)). */ private final Map<String, Map<String, Class<?>>> _actions = new HashMap<String, Map<String, Class<?>>>(); private final Locator _locator; private final ExpressionFactory _xelf; private final SimpleMapper _mapper; private final VariableResolver _resolver; private Map<String, Object> _attrs; private int nLines; /** Whether the page action is defined. */ private boolean pageDefined; //ParseContext// public ExpressionFactory getExpressionFactory() { return _xelf; } public VariableResolver getVariableResolver() { return _resolver; } public FunctionMapper getFunctionMapper() { return _mapper; } //Internal// private Context(String content, XelContext xelc, Locator loc) { this.content = content; _resolver = xelc != null ? xelc.getVariableResolver() : null; _mapper = new SimpleMapper(xelc != null ? xelc.getFunctionMapper() : null); _xelf = Expressions.newExpressionFactory(); _locator = loc; this.nLines = 1; } private boolean hasPrefix(String prefix) { return _actions.containsKey(prefix); } private Class<?> getActionClass(String prefix, String actnm) { final Map<String, Class<?>> acts = _actions.get(prefix); return acts != null ? acts.get(actnm) : null; } private void loadTaglib(String prefix, String uri) throws DspException, IOException { if (_locator == null) throw new DspException("Unable to load " + uri + " because locator is not specified"); URL url = uri.indexOf("://") > 0 ? null : _locator.getResource(uri); if (url == null) { url = Taglibs.getDefaultURL(uri); if (url == null) throw new FileNotFoundException(uri); } try { loadTaglib0(prefix, url); } catch (IOException ex) { throw ex; } catch (Exception ex) { throw DspException.Aide.wrap(ex); } } private void loadTaglib0(String prefix, URL url) throws Exception { final Element root = new SAXBuilder(true, false, true).build(url).getRootElement(); _mapper.load(prefix, url); final Map<String, Class<?>> acts = new HashMap<String, Class<?>>(); for (Iterator it = root.getElements("tag").iterator(); it.hasNext();) { final Element e = (Element) it.next(); final String name = IDOMs.getRequiredElementValue(e, "name"); final String clsName = IDOMs.getRequiredElementValue(e, "tag-class"); final Class<?> cls = Classes.forNameByThread(clsName); if (!Action.class.isAssignableFrom(cls)) throw new DspException(cls + " doesn't implement " + Action.class); acts.put(name, cls); } if (!acts.isEmpty()) _actions.put(prefix, acts); } private Map<String, Object> attrs() { return _attrs != null ? _attrs : (_attrs = new HashMap<String, Object>()); } public Object getAttribute(String name) { return _attrs != null ? _attrs.get(name) : null; } public Object setAttribute(String name, Object value) { return attrs().put(name, value); } public boolean hasAttribute(String name) { return _attrs != null && _attrs.containsKey(name); } public Object removeAttribute(String name) { return _attrs != null ? _attrs.remove(name) : null; } public Map<String, Object> getAttributes() { return attrs(); } } }