/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.components.language.markup.xsp;
import org.apache.cocoon.util.location.LocatedException;
import org.apache.cocoon.util.location.LocationUtils;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
/**
* Parse XSP expressions. Expressions are embedded in attribute="value" and text elements and are
* expanded by the
* {@link org.apache.cocoon.components.language.markup.xsp.XSPMarkupLanguage.PreProcessFilter PreProcessFilter}
* and have the form {#expression}. To prevent interpolation, use {##quote}, which results in the
* text {#quote}.
* An exception is thrown if the closing brace is missing.
* <p>
* The parser has a rudimentary understanding of expressions concerning
* nested braces and braces inside quoted strings and character constants.
* All valid Java, Javascript, and Python expressions can be used.
* <p>
* Example: <h1>Hello {#user.getName()}</h1> <img or
* src="image_{#image.getId()}"/>
* <p>
*
* @version SVN $Id$
*/
public class XSPExpressionParser {
/**
* Handler interface for parsed expressions and text fragments. The parser calls the handler to
* process these.
*/
public static interface Handler {
public void handleText(char[] chars, int start, int length) throws SAXException;
public void handleExpression(char[] chars, int start, int length) throws SAXException;
}
/**
* Parser state.
*/
protected static abstract class State {
/**
* Consume the next character
*
* @param parser The parser
* @param ch The character to consume
* @throws SAXException If there is an error in the expression
*/
public abstract void consume(XSPExpressionParser parser, char ch) throws SAXException;
/**
* Finish processing. Default behaviour is to throw an expression. States that are legal end
* states must overwrite this method.
*
* @param parser The parser
* @throws SAXException It is illegal to finish processing in this state.
*/
public void done(XSPExpressionParser parser) throws SAXException {
throw new SAXException("Incomplete XSP expression {#"+parser.getExpression());
}
}
/**
* Parser state in a quoted string.
*/
protected static class QuotedState extends State {
private final char quote;
/**
* Create state to process quotes strings.
*
* @param quote The quote character to delimit strings
*/
public QuotedState(char quote) {
this.quote = quote;
}
/**
* Consume the next character
*
* @param parser The parser
* @param ch The character to consume
* @throws SAXException If there is an error in the expression
*/
public void consume(XSPExpressionParser parser, char ch) throws SAXException {
parser.append(ch);
if (ch == quote && !parser.isEscaped())
parser.setState(EXPRESSION_STATE);
else if (ch == '\\')
parser.setEscaped(!parser.isEscaped());
else
parser.setEscaped(false);
}
}
/**
* The parser is parsing text.
*/
protected static final State TEXT_STATE = new State() {
public void consume(XSPExpressionParser parser, char ch) throws SAXException {
switch (ch) {
case '{':
parser.setState(LBRACE_STATE);
break;
default:
parser.append(ch);
}
}
/**
* Handle remaining text. It is legal to end in text mode.
*
* @see State#done(XSPExpressionParser)
*/
public void done(XSPExpressionParser parser) throws SAXException {
parser.handleText();
}
};
/**
* The parser has encountered '{' in <code>{@link TEXT_STATE}</code>.
*/
protected static final State LBRACE_STATE = new State() {
public void consume(XSPExpressionParser parser, char ch) throws SAXException {
switch (ch) {
case '#':
parser.setState(TEXT_HASH_STATE);
break;
default:
parser.append('{');
parser.append(ch);
parser.setState(TEXT_STATE);
}
}
/**
* Handle remaining text. It is legal to end text with '{'.
*
* @see State#done(XSPExpressionParser)
*/
public void done(XSPExpressionParser parser) throws SAXException {
// Append the pending '{'
parser.append('{');
parser.handleText();
}
};
/**
* The parser has encountered '#' in <code>{@link LBRACE_STATE}</code>.
*/
protected static final State TEXT_HASH_STATE = new State() {
public void consume(XSPExpressionParser parser, char ch) throws SAXException {
switch (ch) {
case '#':
parser.append('{');
parser.append('#');
parser.setState(TEXT_STATE);
break;
default:
parser.handleText();
parser.initExpression();
parser.setState(EXPRESSION_STATE);
EXPRESSION_STATE.consume(parser, ch);
}
}
};
/**
* The parser is parsing an expression.
*/
protected static final State EXPRESSION_STATE = new State() {
public void consume(XSPExpressionParser parser, char ch) throws SAXException {
switch (ch) {
case '{':
parser.incrNesting();
parser.append(ch);
break;
case '}':
if (parser.decrNesting() > 0) {
parser.append(ch);
}
else {
parser.handleExpression();
parser.setState(TEXT_STATE);
}
break;
case '"':
parser.append(ch);
parser.setState(EXPRESSION_STRING_STATE);
break;
case '\'':
parser.append(ch);
parser.setState(EXPRESSION_CHAR_STATE);
break;
case '\u00B4':
parser.append(ch);
parser.setState(EXPRESSION_SHELL_STATE);
break;
default:
parser.append(ch);
}
}
};
/**
* The parser has encountered '"' in <code>{@link EXPRESSION_STATE}</code>
* to start a string constant.
*/
protected static final State EXPRESSION_STRING_STATE = new QuotedState('"');
/**
* The parser has encountered '\'' in <code>{@link EXPRESSION_STATE}</code>
* to start a character constant.
*/
protected static final State EXPRESSION_CHAR_STATE = new QuotedState('\'');
/**
* The parser has encountered '`' (Backtick, ASCII 0x60) in
* <code>{@link EXPRESSION_STATE}</code> to start a Python string constant.
*/
protected static final State EXPRESSION_SHELL_STATE = new QuotedState('`');
/**
* The parser state
*/
private State state = TEXT_STATE;
/**
* The nesting level of braces.
*/
private int nesting = 0;
/**
* Flag whether previous character was a backslash to escape quotes.
*/
private boolean escaped = false;
/**
* The handler for parsed text and expression fragments.
*/
private Handler handler;
/**
* The buffer for the current text or expression fragment. We do our own StringBuffer here to
* save some allocations of char arrays for the handler.
*/
private char[] buf = new char[256];
/**
* The current size of the fragment in the buffer.
*/
private int bufSize;
/**
* The delty by which the buffer grows if it is too small.
*/
private int bufGrow = 256;
/**
* Create a new <code>{@link XSPExpressionParser}</code>.
*
* @param handler The handler for parsed text and expression fragments.
*/
public XSPExpressionParser(Handler handler) {
this.handler = handler;
}
/**
* Parses a character sequence.
*
* @param chars The character sequence to parse
* @throws SAXException If there is an error in the sequence.
*/
public void consume(String chars) throws SAXException {
int end = chars.length();
for (int i = 0; i < end; ++i) {
char ch = chars.charAt(i);
state.consume(this, ch);
}
}
/**
* Parses part of a character array.
*
* @param chars The characters
* @param start The start position in the character array
* @param length The number of characters to parse
* @throws SAXException If there is an error in the sequence.
*/
public void consume(char[] chars, int start, int length) throws SAXException {
int end = start + length;
for (int i = start; i < end; ++i) {
char ch = chars[i];
state.consume(this, ch);
}
}
/**
* Flushes the parser
*
* @param locator The SAX locator to determine the current parse position
* @param description The description of the current parse context
* @throws SAXException If there is an error in the parsed text.
* A wrapped LocatedException contains the location of the parse error.
*/
public void flush(Locator locator, String description) throws SAXException {
try {
state.done(this);
bufSize = 0;
state = TEXT_STATE;
}
catch(SAXException ex) {
throw new SAXException(new LocatedException(ex.getMessage(), ex,
LocationUtils.getLocation(locator, description)));
}
}
protected State getState() {
return state;
}
protected void setState(State state) {
this.state = state;
}
protected void initExpression() {
nesting = 1;
escaped = false;
}
protected void incrNesting() {
nesting++;
}
protected int decrNesting() {
return --nesting;
}
protected void setEscaped(boolean escaped) {
this.escaped = escaped;
}
protected boolean isEscaped() {
return escaped;
}
protected String getExpression() {
return new String(buf, 0, bufSize);
}
protected void handleText() throws SAXException {
if (bufSize > 0) {
handler.handleText(buf, 0, bufSize);
bufSize = 0;
}
}
protected void handleExpression() throws SAXException {
if (bufSize == 0) {
throw new SAXException("Illegal empty expression.");
}
handler.handleExpression(buf, 0, bufSize);
bufSize = 0;
}
protected void append(char ch) {
if (bufSize + 1 >= buf.length) {
char[] newBuf = new char[buf.length + bufGrow];
System.arraycopy(buf, 0, newBuf, 0, buf.length);
buf = newBuf;
}
buf[bufSize] = ch;
++bufSize;
}
}