001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.language.markup.xsp;
018:
019: import org.apache.cocoon.util.location.LocatedException;
020: import org.apache.cocoon.util.location.LocationUtils;
021:
022: import org.xml.sax.Locator;
023: import org.xml.sax.SAXException;
024:
025: /**
026: * Parse XSP expressions. Expressions are embedded in attribute="value" and text elements and are
027: * expanded by the
028: * {@link org.apache.cocoon.components.language.markup.xsp.XSPMarkupLanguage.PreProcessFilter PreProcessFilter}
029: * and have the form {#expression}. To prevent interpolation, use {##quote}, which results in the
030: * text {#quote}.
031: * An exception is thrown if the closing brace is missing.
032: * <p>
033: * The parser has a rudimentary understanding of expressions concerning
034: * nested braces and braces inside quoted strings and character constants.
035: * All valid Java, Javascript, and Python expressions can be used.
036: * <p>
037: * Example: <h1>Hello {#user.getName()}</h1> <img or
038: * src="image_{#image.getId()}"/>
039: * <p>
040: *
041: * @version SVN $Id: XSPExpressionParser.java 433543 2006-08-22 06:22:54Z crossley $
042: */
043: public class XSPExpressionParser {
044:
045: /**
046: * Handler interface for parsed expressions and text fragments. The parser calls the handler to
047: * process these.
048: */
049: public static interface Handler {
050: public void handleText(char[] chars, int start, int length)
051: throws SAXException;
052:
053: public void handleExpression(char[] chars, int start, int length)
054: throws SAXException;
055: }
056:
057: /**
058: * Parser state.
059: */
060: protected static abstract class State {
061: /**
062: * Consume the next character
063: *
064: * @param parser The parser
065: * @param ch The character to consume
066: * @throws SAXException If there is an error in the expression
067: */
068: public abstract void consume(XSPExpressionParser parser, char ch)
069: throws SAXException;
070:
071: /**
072: * Finish processing. Default behaviour is to throw an expression. States that are legal end
073: * states must overwrite this method.
074: *
075: * @param parser The parser
076: * @throws SAXException It is illegal to finish processing in this state.
077: */
078: public void done(XSPExpressionParser parser)
079: throws SAXException {
080: throw new SAXException("Incomplete XSP expression {#"
081: + parser.getExpression());
082: }
083: }
084:
085: /**
086: * Parser state in a quoted string.
087: */
088: protected static class QuotedState extends State {
089: private final char quote;
090:
091: /**
092: * Create state to process quotes strings.
093: *
094: * @param quote The quote character to delimit strings
095: */
096: public QuotedState(char quote) {
097: this .quote = quote;
098: }
099:
100: /**
101: * Consume the next character
102: *
103: * @param parser The parser
104: * @param ch The character to consume
105: * @throws SAXException If there is an error in the expression
106: */
107: public void consume(XSPExpressionParser parser, char ch)
108: throws SAXException {
109: parser.append(ch);
110: if (ch == quote && !parser.isEscaped())
111: parser.setState(EXPRESSION_STATE);
112: else if (ch == '\\')
113: parser.setEscaped(!parser.isEscaped());
114: else
115: parser.setEscaped(false);
116: }
117: }
118:
119: /**
120: * The parser is parsing text.
121: */
122: protected static final State TEXT_STATE = new State() {
123: public void consume(XSPExpressionParser parser, char ch)
124: throws SAXException {
125: switch (ch) {
126: case '{':
127: parser.setState(LBRACE_STATE);
128: break;
129:
130: default:
131: parser.append(ch);
132: }
133: }
134:
135: /**
136: * Handle remaining text. It is legal to end in text mode.
137: *
138: * @see State#done(XSPExpressionParser)
139: */
140: public void done(XSPExpressionParser parser)
141: throws SAXException {
142: parser.handleText();
143: }
144: };
145:
146: /**
147: * The parser has encountered '{' in <code>{@link TEXT_STATE}</code>.
148: */
149: protected static final State LBRACE_STATE = new State() {
150: public void consume(XSPExpressionParser parser, char ch)
151: throws SAXException {
152: switch (ch) {
153: case '#':
154: parser.setState(TEXT_HASH_STATE);
155: break;
156:
157: default:
158: parser.append('{');
159: parser.append(ch);
160: parser.setState(TEXT_STATE);
161: }
162: }
163:
164: /**
165: * Handle remaining text. It is legal to end text with '{'.
166: *
167: * @see State#done(XSPExpressionParser)
168: */
169: public void done(XSPExpressionParser parser)
170: throws SAXException {
171: // Append the pending '{'
172: parser.append('{');
173: parser.handleText();
174: }
175: };
176:
177: /**
178: * The parser has encountered '#' in <code>{@link LBRACE_STATE}</code>.
179: */
180: protected static final State TEXT_HASH_STATE = new State() {
181: public void consume(XSPExpressionParser parser, char ch)
182: throws SAXException {
183: switch (ch) {
184: case '#':
185: parser.append('{');
186: parser.append('#');
187: parser.setState(TEXT_STATE);
188: break;
189:
190: default:
191: parser.handleText();
192: parser.initExpression();
193: parser.setState(EXPRESSION_STATE);
194: EXPRESSION_STATE.consume(parser, ch);
195: }
196: }
197: };
198:
199: /**
200: * The parser is parsing an expression.
201: */
202: protected static final State EXPRESSION_STATE = new State() {
203: public void consume(XSPExpressionParser parser, char ch)
204: throws SAXException {
205: switch (ch) {
206: case '{':
207: parser.incrNesting();
208: parser.append(ch);
209: break;
210:
211: case '}':
212: if (parser.decrNesting() > 0) {
213: parser.append(ch);
214: } else {
215: parser.handleExpression();
216: parser.setState(TEXT_STATE);
217: }
218: break;
219:
220: case '"':
221: parser.append(ch);
222: parser.setState(EXPRESSION_STRING_STATE);
223: break;
224:
225: case '\'':
226: parser.append(ch);
227: parser.setState(EXPRESSION_CHAR_STATE);
228: break;
229:
230: case '\u00B4':
231: parser.append(ch);
232: parser.setState(EXPRESSION_SHELL_STATE);
233: break;
234:
235: default:
236: parser.append(ch);
237: }
238: }
239: };
240:
241: /**
242: * The parser has encountered '"' in <code>{@link EXPRESSION_STATE}</code>
243: * to start a string constant.
244: */
245: protected static final State EXPRESSION_STRING_STATE = new QuotedState(
246: '"');
247:
248: /**
249: * The parser has encountered '\'' in <code>{@link EXPRESSION_STATE}</code>
250: * to start a character constant.
251: */
252: protected static final State EXPRESSION_CHAR_STATE = new QuotedState(
253: '\'');
254:
255: /**
256: * The parser has encountered '`' (Backtick, ASCII 0x60) in
257: * <code>{@link EXPRESSION_STATE}</code> to start a Python string constant.
258: */
259: protected static final State EXPRESSION_SHELL_STATE = new QuotedState(
260: '`');
261:
262: /**
263: * The parser state
264: */
265: private State state = TEXT_STATE;
266:
267: /**
268: * The nesting level of braces.
269: */
270: private int nesting = 0;
271:
272: /**
273: * Flag whether previous character was a backslash to escape quotes.
274: */
275: private boolean escaped = false;
276:
277: /**
278: * The handler for parsed text and expression fragments.
279: */
280: private Handler handler;
281:
282: /**
283: * The buffer for the current text or expression fragment. We do our own StringBuffer here to
284: * save some allocations of char arrays for the handler.
285: */
286: private char[] buf = new char[256];
287:
288: /**
289: * The current size of the fragment in the buffer.
290: */
291: private int bufSize;
292:
293: /**
294: * The delty by which the buffer grows if it is too small.
295: */
296: private int bufGrow = 256;
297:
298: /**
299: * Create a new <code>{@link XSPExpressionParser}</code>.
300: *
301: * @param handler The handler for parsed text and expression fragments.
302: */
303: public XSPExpressionParser(Handler handler) {
304: this .handler = handler;
305: }
306:
307: /**
308: * Parses a character sequence.
309: *
310: * @param chars The character sequence to parse
311: * @throws SAXException If there is an error in the sequence.
312: */
313: public void consume(String chars) throws SAXException {
314: int end = chars.length();
315:
316: for (int i = 0; i < end; ++i) {
317: char ch = chars.charAt(i);
318: state.consume(this , ch);
319: }
320: }
321:
322: /**
323: * Parses part of a character array.
324: *
325: * @param chars The characters
326: * @param start The start position in the character array
327: * @param length The number of characters to parse
328: * @throws SAXException If there is an error in the sequence.
329: */
330: public void consume(char[] chars, int start, int length)
331: throws SAXException {
332: int end = start + length;
333:
334: for (int i = start; i < end; ++i) {
335: char ch = chars[i];
336: state.consume(this , ch);
337: }
338: }
339:
340: /**
341: * Flushes the parser
342: *
343: * @param locator The SAX locator to determine the current parse position
344: * @param description The description of the current parse context
345: * @throws SAXException If there is an error in the parsed text.
346: * A wrapped LocatedException contains the location of the parse error.
347: */
348: public void flush(Locator locator, String description)
349: throws SAXException {
350: try {
351: state.done(this );
352: bufSize = 0;
353: state = TEXT_STATE;
354: } catch (SAXException ex) {
355: throw new SAXException(new LocatedException(
356: ex.getMessage(), ex, LocationUtils.getLocation(
357: locator, description)));
358: }
359: }
360:
361: protected State getState() {
362: return state;
363: }
364:
365: protected void setState(State state) {
366: this .state = state;
367: }
368:
369: protected void initExpression() {
370: nesting = 1;
371: escaped = false;
372: }
373:
374: protected void incrNesting() {
375: nesting++;
376: }
377:
378: protected int decrNesting() {
379: return --nesting;
380: }
381:
382: protected void setEscaped(boolean escaped) {
383: this .escaped = escaped;
384: }
385:
386: protected boolean isEscaped() {
387: return escaped;
388: }
389:
390: protected String getExpression() {
391: return new String(buf, 0, bufSize);
392: }
393:
394: protected void handleText() throws SAXException {
395: if (bufSize > 0) {
396: handler.handleText(buf, 0, bufSize);
397: bufSize = 0;
398: }
399: }
400:
401: protected void handleExpression() throws SAXException {
402: if (bufSize == 0) {
403: throw new SAXException("Illegal empty expression.");
404: }
405:
406: handler.handleExpression(buf, 0, bufSize);
407:
408: bufSize = 0;
409: }
410:
411: protected void append(char ch) {
412: if (bufSize + 1 >= buf.length) {
413: char[] newBuf = new char[buf.length + bufGrow];
414: System.arraycopy(buf, 0, newBuf, 0, buf.length);
415: buf = newBuf;
416: }
417:
418: buf[bufSize] = ch;
419: ++bufSize;
420: }
421: }
|