001: /* Parser.java
002:
003: {{IS_NOTE
004: Purpose:
005:
006: Description:
007:
008: History:
009: Sat Sep 17 12:12:41 2005, Created by tomyeh
010: }}IS_NOTE
011:
012: Copyright (C) 2004 Potix Corporation. All Rights Reserved.
013:
014: {{IS_RIGHT
015: This program is distributed under GPL Version 2.0 in the hope that
016: it will be useful, but WITHOUT ANY WARRANTY.
017: }}IS_RIGHT
018: */
019: package org.zkoss.web.servlet.dsp.impl;
020:
021: import java.util.Map;
022: import java.util.HashMap;
023: import java.util.Iterator;
024: import java.io.IOException;
025: import java.io.FileNotFoundException;
026: import java.net.URL;
027:
028: import org.zkoss.lang.D;
029: import org.zkoss.lang.Classes;
030: import org.zkoss.util.logging.Log;
031: import org.zkoss.util.resource.Locator;
032: import org.zkoss.xel.Expressions;
033: import org.zkoss.xel.ExpressionFactory;
034: import org.zkoss.xel.XelContext;
035: import org.zkoss.xel.VariableResolver;
036: import org.zkoss.xel.FunctionMapper;
037: import org.zkoss.xel.XelException;
038: import org.zkoss.xel.util.SimpleMapper;
039: import org.zkoss.xel.taglib.Taglibs;
040: import org.zkoss.idom.input.SAXBuilder;
041: import org.zkoss.idom.Element;
042: import org.zkoss.idom.util.IDOMs;
043:
044: import org.zkoss.web.mesg.MWeb;
045: import org.zkoss.web.servlet.ServletException;
046: import org.zkoss.web.servlet.dsp.*;
047: import org.zkoss.web.servlet.dsp.action.Page;
048: import org.zkoss.web.servlet.dsp.action.Action;
049:
050: /**
051: * Used to parse a DSP page into a meta format called
052: * {@link Interpretation}.
053: *
054: * @author tomyeh
055: */
056: public class Parser {
057: // private static final Log log = Log.lookup(Parser.class);
058:
059: /** Parses the content into a meta format
060: *
061: * @param content the content to parse; never null.
062: * @param ctype the content type. Optional. It is used only if
063: * no page action at all. If it is not specified and not page
064: * action, "text/html" is assumed.
065: * @param xelc the context information used to parse XEL expressions
066: * in the content.
067: * @param loc used to locate the resource such as taglib.
068: * It could null only if DSP contains no such resource.
069: */
070: public Interpretation parse(String content, String ctype,
071: XelContext xelc, Locator loc)
072: throws javax.servlet.ServletException, IOException,
073: XelException {
074: final Context ctx = new Context(content, xelc, loc);
075: final RootNode root = new RootNode();
076: parse0(ctx, root, 0, content.length());
077:
078: if (!ctx.pageDefined) {
079: //We always create a page definition
080: // if (D.ON && log.debugable()) log.debug("Use default content type: "+ctype);
081: final ActionNode action = new ActionNode(Page.class, 0);
082: root.addChild(0, action);
083: final Map attrs = new HashMap(2);
084: attrs.put("contentType", ctype != null ? ctype
085: : "text/html");
086: applyAttrs("page", action, attrs, ctx);
087: }
088:
089: return root;
090: }
091:
092: /** Recursively parse the content into a tree of {@link Node}.
093: */
094: private static void parse0(Context ctx, Node parent, int from,
095: int to) throws javax.servlet.ServletException, IOException,
096: XelException {
097: boolean esc = false;
098: final StringBuffer sb = new StringBuffer(512);
099: for (int j = from; j < to; ++j) {
100: char cc = ctx.content.charAt(j);
101: //We only recognize <%, <\%, ${, $\{ and <xx:yy>
102: switch (cc) {
103: case '<':
104: if (j + 1 < to) {
105: char c2 = ctx.content.charAt(j + 1);
106: if (c2 == '\\') {
107: if (j + 2 < to
108: && ctx.content.charAt(j + 2) == '%')
109: ++j; //skip '\\'
110: } else if (c2 == '%') {
111: addText(parent, sb);
112: j = parseControl(ctx, parent, j, to);
113: continue;
114: } else {
115: final int oldLines = ctx.nLines;
116: int k = skipWhitespaces(ctx, j + 1, to);
117: int l = nextSeparator(ctx, k, to);
118: if (l >= to || l == k
119: || ctx.content.charAt(l) != ':') {
120: ctx.nLines = oldLines;
121: break; //bypass what we don't recognize
122: }
123: final String prefix = ctx.content.substring(k,
124: l);
125: if (!ctx.hasPrefix(prefix)) {
126: ctx.nLines = oldLines;
127: break; //bypass what we don't recognize
128: }
129:
130: addText(parent, sb);
131: j = parseAction(ctx, parent, prefix, l, to);
132: continue;
133: }
134: }
135: break;
136: case '$':
137: if (j + 1 < to) {
138: char c2 = ctx.content.charAt(j + 1);
139: if (c2 == '\\') {
140: if (j + 2 < to
141: && ctx.content.charAt(j + 2) == '{')
142: ++j; //skip '\\'
143: } else if (c2 == '{') {
144: addText(parent, sb);
145: j = parseEL(ctx, parent, j, to);
146: continue;
147: }
148: }
149: break;
150: case '\n':
151: ++ctx.nLines;
152: }
153: sb.append(cc);
154: }
155: addText(parent, sb);
156: }
157:
158: /** Parses a control (e.g., <% page %>) starting at from,
159: * and returns the postion of '>' (in %>).
160: */
161: private static int parseControl(Context ctx, Node parent, int from,
162: int to) throws javax.servlet.ServletException, IOException,
163: XelException {
164: int j = from + 2;
165: if (j + 1 >= to)
166: throw new ServletException(MWeb.DSP_ACTION_NOT_TERMINATED,
167: new Object[] { null, new Integer(ctx.nLines) });
168:
169: //0. comment
170: char cc = ctx.content.charAt(j);
171: if (cc == '-' && ctx.content.charAt(j + 1) == '-') { //comment
172: for (int end = to - 4;; ++j) {
173: if (j > end)
174: throw new ServletException(
175: MWeb.DSP_COMMENT_NOT_TERMINATED,
176: new Integer(ctx.nLines));
177: if (ctx.content.charAt(j) == '\n')
178: ++ctx.nLines;
179: else if (startsWith(ctx.content, j, to, "--%>"))
180: return j + 3;
181: }
182: }
183: if (cc != '@')
184: throw new ServletException(MWeb.DSP_EXPECT_CHARACTER,
185: new Object[] { new Character('@'),
186: new Integer(ctx.nLines) });
187:
188: //1: which control
189: j = skipWhitespaces(ctx, j + 1, to);
190: int k = nextSeparator(ctx, j, to);
191: if (k >= to)
192: throw new ServletException(MWeb.DSP_ACTION_NOT_TERMINATED,
193: new Object[] { null, new Integer(ctx.nLines) });
194: final ActionNode action;
195: final String ctlnm = ctx.content.substring(j, k);
196: if ("taglib".equals(ctlnm)) {
197: action = null;
198: } else if ("page".equals(ctlnm)) {
199: ctx.pageDefined = true;
200: trim(parent); //Bug 1798123: avoid getOut being called before Page
201: parent.addChild(action = new ActionNode(Page.class,
202: ctx.nLines));
203: } else {
204: throw new ServletException(MWeb.DSP_UNKNOWN_ACTION,
205: new Object[] { ctlnm, new Integer(ctx.nLines) });
206: }
207:
208: //2: parse attributes
209: final Map attrs = new HashMap();
210: k = parseAttrs(ctx, attrs, ctlnm, k, to);
211: cc = ctx.content.charAt(k);
212: if (cc != '%')
213: throw new ServletException(MWeb.DSP_EXPECT_CHARACTER,
214: new Object[] { new Character('%'),
215: new Integer(ctx.nLines) });
216:
217: if (action == null) { //taglib
218: final String uri = (String) attrs.get("uri"), prefix = (String) attrs
219: .get("prefix");
220: if (prefix == null || uri == null)
221: throw new ServletException(
222: MWeb.DSP_TAGLIB_ATTRIBUTE_REQUIRED,
223: new Integer(ctx.nLines));
224: ctx.loadTaglib(prefix, uri);
225: } else {
226: applyAttrs(ctlnm, action, attrs, ctx);
227: }
228:
229: if (++k >= to || ctx.content.charAt(k) != '>')
230: throw new ServletException(MWeb.DSP_ACTION_NOT_TERMINATED,
231: new Object[] { ctlnm, new Integer(ctx.nLines) });
232: return k;
233: }
234:
235: /** Trimmed {@link TextNode} that contains nothing but spaces.
236: */
237: private static void trim(Node node) {
238: for (Iterator it = node.getChildren().iterator(); it.hasNext();) {
239: final Object o = it.next();
240: if (o instanceof TextNode) {
241: final String s = ((TextNode) o).getText();
242: if (s == null || s.trim().length() == 0)
243: it.remove();
244: }
245: }
246: }
247:
248: /** Parses an action (e.g., <c:forEach...>...</c:forEach>).
249: * @param from the position of ':'
250: * @return the postion of the last '>'.
251: */
252: private static int parseAction(Context ctx, Node parent,
253: String prefix, int from, int to)
254: throws javax.servlet.ServletException, IOException,
255: XelException {
256: //1: which action
257: int j = skipWhitespaces(ctx, from + 1, to);
258: int k = nextSeparator(ctx, j, to);
259: if (k >= to)
260: throw new ServletException(MWeb.DSP_ACTION_NOT_TERMINATED,
261: new Object[] { prefix + ':',
262: new Integer(ctx.nLines) });
263: if (k == j)
264: throw new ServletException(MWeb.DSP_ACTION_REQUIRED,
265: new Integer(ctx.nLines));
266:
267: final String actnm = ctx.content.substring(j, k);
268: final Class actcls = ctx.getActionClass(prefix, actnm);
269: if (actcls == null)
270: throw new ServletException(MWeb.DSP_UNKNOWN_ACTION,
271: new Object[] { prefix + ':' + actnm,
272: new Integer(ctx.nLines) });
273: final ActionNode action = new ActionNode(actcls, ctx.nLines);
274: parent.addChild(action);
275: // if (D.ON && log.debugable()) log.debug("Action "+actnm);
276:
277: //2: action's attributes
278: final Map attrs = new HashMap();
279: j = parseAttrs(ctx, attrs, actnm, k, to);
280: char cc = ctx.content.charAt(j);
281: boolean ended = cc == '/';
282: if (!ended && cc != '>')
283: throw new ServletException(MWeb.DSP_UNEXPECT_CHARACTER,
284: new Object[] { new Character(cc),
285: new Integer(ctx.nLines) });
286:
287: applyAttrs(actnm, action, attrs, ctx);
288:
289: if (ended) {
290: if (j + 1 >= to || ctx.content.charAt(j + 1) != '>')
291: throw new ServletException(
292: MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] {
293: prefix + ':' + actnm,
294: new Integer(action.getLineNumber()) });
295: return j + 1;
296: }
297:
298: //3: nested content
299: final int nestedFrom = ++j, nestedTo;
300: for (int depth = 0;; ++j) {
301: if (j >= to)
302: throw new ServletException(
303: MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] {
304: actnm,
305: new Integer(action.getLineNumber()) });
306:
307: cc = ctx.content.charAt(j);
308: if (j + 1 < to) {
309: if (cc == '<') {
310: final int oldLines = ctx.nLines;
311: k = j + 1;
312: ended = ctx.content.charAt(k) == '/';
313: k = skipWhitespaces(ctx, ended ? k + 1 : k, to);
314: int l = nextSeparator(ctx, k, to);
315: if (l >= to
316: || ctx.content.charAt(l) != ':'
317: || !prefix.equals(ctx.content.substring(k,
318: l))) {
319: ctx.nLines = oldLines;
320: continue; //bypass
321: }
322:
323: k = skipWhitespaces(ctx, l + 1, to);
324: l = nextSeparator(ctx, k, to);
325: if (l >= to
326: || !actnm.equals(ctx.content
327: .substring(k, l))) {
328: ctx.nLines = oldLines;
329: continue; //bypass
330: }
331: l = skipWhitespaces(ctx, l, to);
332: if (l >= to
333: || (ended && ctx.content.charAt(l) != '>')) {
334: ctx.nLines = oldLines;
335: continue; //bypass
336: }
337:
338: if (ended) {
339: if (--depth < 0) {
340: nestedTo = j;
341: j = l;
342: break; //done
343: }
344: } else {
345: ++depth;
346: }
347: j = l;
348: continue;
349: } else if (cc == '$'
350: && ctx.content.charAt(j + 1) == '{') {
351: j = endOfEL(ctx, j, to);
352: continue;
353: }
354: }
355: if (cc == '\n')
356: ++ctx.nLines;
357: }
358:
359: parse0(ctx, action, nestedFrom, nestedTo); //recursive
360: return j;
361: }
362:
363: private static boolean startsWith(String content, int from, int to,
364: String s) {
365: for (int j = 0, len = s.length();; ++from, ++j) {
366: if (j >= len)
367: return true;
368: if (from >= to || content.charAt(from) != s.charAt(j))
369: return false;
370: }
371: }
372:
373: private static int skipWhitespaces(Context ctx, int from, int to) {
374: for (; from < to; ++from) {
375: final char cc = ctx.content.charAt(from);
376: if (cc == '\n')
377: ++ctx.nLines;
378: else if (!Character.isWhitespace(cc))
379: break;
380: }
381: return from;
382: }
383:
384: private static int nextSeparator(Context ctx, int from, int to) {
385: for (; from < to; ++from) {
386: final char cc = ctx.content.charAt(from);
387: if ((cc < '0' || cc > '9') && (cc < 'a' || cc > 'z')
388: && (cc < 'A' || cc > 'Z') && cc != '_')
389: break;
390: }
391: return from;
392: }
393:
394: /** Parses the attributes.
395: */
396: private static int parseAttrs(Context ctx, Map attrs, String actnm,
397: int from, int to) throws javax.servlet.ServletException {
398: for (int j, k = from;;) {
399: j = skipWhitespaces(ctx, k, to);
400: k = nextSeparator(ctx, j, to);
401: if (k >= to)
402: throw new ServletException(
403: MWeb.DSP_ACTION_NOT_TERMINATED, new Object[] {
404: actnm, new Integer(ctx.nLines) });
405: if (j == k)
406: return j;
407:
408: final String attrnm = ctx.content.substring(j, k);
409: k = skipWhitespaces(ctx, k, to);
410: j = skipWhitespaces(ctx, k + 1, to);
411: if (j >= to || ctx.content.charAt(k) != '=')
412: throw new ServletException(
413: MWeb.DSP_ATTRIBUTE_VALUE_REQUIRED,
414: new Object[] { actnm, attrnm,
415: new Integer(ctx.nLines) });
416:
417: final char quot = ctx.content.charAt(j);
418: if (quot != '"' && quot != '\'')
419: throw new ServletException(
420: MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED,
421: new Object[] { actnm, attrnm,
422: new Integer(ctx.nLines) });
423:
424: final StringBuffer sbval = new StringBuffer();
425: for (k = ++j;; ++k) {
426: if (k >= to)
427: throw new ServletException(
428: MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED,
429: new Object[] { actnm, attrnm,
430: new Integer(ctx.nLines) });
431: final char cc = ctx.content.charAt(k);
432: if (cc == '\n')
433: throw new ServletException(
434: MWeb.DSP_ATTRIBUTE_VALUE_QUOTE_REQUIRED,
435: new Object[] { actnm, attrnm,
436: new Integer(ctx.nLines) });
437:
438: if (cc == quot) {
439: ++k;
440: break; //found
441: }
442:
443: sbval.append(cc);
444: if (cc == '\\' && ++k < to)
445: sbval.setCharAt(sbval.length() - 1, ctx.content
446: .charAt(k));
447: }
448:
449: attrs.put(attrnm, sbval.toString());
450: }
451: }
452:
453: /** Applies attributes.
454: */
455: private static final void applyAttrs(String actnm,
456: ActionNode action, Map attrs, ParseContext ctx)
457: throws javax.servlet.ServletException, XelException {
458: for (Iterator it = attrs.entrySet().iterator(); it.hasNext();) {
459: final Map.Entry me = (Map.Entry) it.next();
460: final String attrnm = (String) me.getKey();
461: final String attrval = (String) me.getValue();
462: try {
463: action.addAttribute(attrnm, attrval, ctx);
464: } catch (NoSuchMethodException ex) {
465: throw new ServletException(
466: MWeb.DSP_ATTRIBUTE_NOT_FOUND, new Object[] {
467: actnm, attrnm,
468: new Integer(action.getLineNumber()) });
469: } catch (ClassCastException ex) {
470: throw new ServletException(
471: MWeb.DSP_ATTRIBUTE_INVALID_VALUE, new Object[] {
472: actnm, attrnm, attrval,
473: new Integer(action.getLineNumber()) },
474: ex);
475: }
476: }
477: }
478:
479: /** Parses an EL expression starting at from.
480: * @return the position of }.
481: */
482: private static int parseEL(Context ctx, Node parent, int from,
483: int to) throws javax.servlet.ServletException, XelException {
484: int j = endOfEL(ctx, from, to); //point to }
485: parent.addChild(new XelNode(ctx.content.substring(from, j + 1),
486: ctx));
487: return j;
488: }
489:
490: /** Returns the position of '}'. */
491: private static int endOfEL(Context ctx, int from, int to)
492: throws javax.servlet.ServletException {
493: for (int j = from + 2;; ++j) {
494: if (j >= to)
495: throw new ServletException(MWeb.EL_NOT_TERMINATED,
496: new Integer(ctx.nLines));
497:
498: final char cc = ctx.content.charAt(j);
499: if (cc == '}') {
500: return j;
501: } else if (cc == '\'' || cc == '"') {
502: while (++j < to) {
503: final char c2 = ctx.content.charAt(j);
504: if (c2 == cc)
505: break;
506: if (cc == '\n')
507: throw new ServletException(
508: "Illegal EL expression: non-terminaled "
509: + cc + " at line " + ctx.nLines
510: + " character " + j);
511: if (c2 == '\\' && ++j < to
512: && ctx.content.charAt(j) == '\n')
513: ++ctx.nLines;
514: }
515: } else if (cc == '\n') {
516: ++ctx.nLines;
517: }
518: }
519: }
520:
521: /** Adds a text node. */
522: private static void addText(Node parent, StringBuffer sb) {
523: if (sb.length() > 0) {
524: parent.addChild(new TextNode(sb.toString()));
525: sb.setLength(0);
526: }
527: }
528:
529: /** Context used for parsing. */
530: private static class Context implements ParseContext {
531: private final String content;
532: /** (String prefix, Map(String name, Class class)). */
533: private final Map _actions = new HashMap();
534: private final Locator _locator;
535: private final ExpressionFactory _xelf;
536: private final SimpleMapper _mapper;
537: private final VariableResolver _resolver;
538: private int nLines;
539: /** Whether the page action is defined. */
540: private boolean pageDefined;
541:
542: //ParseContext//
543: public ExpressionFactory getExpressionFactory() {
544: return _xelf;
545: }
546:
547: public VariableResolver getVariableResolver() {
548: return _resolver;
549: }
550:
551: public FunctionMapper getFunctionMapper() {
552: return _mapper;
553: }
554:
555: //Internal//
556: private Context(String content, XelContext xelc, Locator loc) {
557: this .content = content;
558: _resolver = xelc != null ? xelc.getVariableResolver()
559: : null;
560: _mapper = new SimpleMapper(xelc != null ? xelc
561: .getFunctionMapper() : null);
562: _xelf = Expressions.newExpressionFactory();
563: _locator = loc;
564: this .nLines = 1;
565: }
566:
567: private boolean hasPrefix(String prefix) {
568: return _actions.containsKey(prefix);
569: }
570:
571: private Class getActionClass(String prefix, String actnm) {
572: final Map acts = (Map) _actions.get(prefix);
573: return acts != null ? (Class) acts.get(actnm) : null;
574: }
575:
576: private void loadTaglib(String prefix, String uri)
577: throws javax.servlet.ServletException, IOException {
578: // if (D.ON && log.debugable()) log.debug("Loading "+prefix+" at "+uri);
579: if (_locator == null)
580: throw new ServletException("Unable to load " + uri
581: + " because locator is not specified");
582:
583: URL url = uri.indexOf("://") > 0 ? null : _locator
584: .getResource(uri);
585: if (url == null) {
586: url = Taglibs.getDefaultURL(uri);
587: if (url == null)
588: throw new FileNotFoundException(uri);
589: }
590:
591: try {
592: loadTaglib0(prefix, url);
593: } catch (IOException ex) {
594: throw ex;
595: } catch (Exception ex) {
596: throw ServletException.Aide.wrap(ex);
597: }
598: }
599:
600: private void loadTaglib0(String prefix, URL url)
601: throws Exception {
602: final Element root = new SAXBuilder(true, false, true)
603: .build(url).getRootElement();
604: _mapper.load(prefix, root);
605:
606: final Map acts = new HashMap();
607: for (Iterator it = root.getElements("tag").iterator(); it
608: .hasNext();) {
609: final Element e = (Element) it.next();
610: final String name = IDOMs.getRequiredElementValue(e,
611: "name");
612: final String clsName = IDOMs.getRequiredElementValue(e,
613: "tag-class");
614: final Class cls = Classes.forNameByThread(clsName);
615: if (!Action.class.isAssignableFrom(cls))
616: throw new ServletException(cls
617: + " doesn't implement " + Action.class);
618: acts.put(name, cls);
619: // if (D.ON && log.finerable()) log.finer("Action "+prefix+":"+name+" are added");
620: }
621: if (!acts.isEmpty())
622: _actions.put(prefix, acts);
623: }
624: }
625: }
|