/* * FreeMarker: a tool that allows Java programs to generate HTML * output using templates. * Copyright (C) 1998-2004 Benjamin Geer * Email: beroul@users.sourceforge.net * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library 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. * * 22 October 1999: Modified by Holger Arendt to parse method calls * in expressions. */ package freemarker.template.compiler; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import freemarker.template.FunctionTemplateProcessor; import freemarker.template.InputSource; import freemarker.template.TemplateException; import freemarker.template.expression.And; import freemarker.template.expression.BooleanLiteral; import freemarker.template.expression.Divide; import freemarker.template.expression.Dot; import freemarker.template.expression.DynamicKeyName; import freemarker.template.expression.EmptyLiteral; import freemarker.template.expression.Equals; import freemarker.template.expression.Expression; import freemarker.template.expression.ExpressionBuilder; import freemarker.template.expression.GreaterThan; import freemarker.template.expression.GreaterThanOrEquals; import freemarker.template.expression.HashLiteral; import freemarker.template.expression.Identifier; import freemarker.template.expression.LessThan; import freemarker.template.expression.LessThanOrEquals; import freemarker.template.expression.ListLiteral; import freemarker.template.expression.ListRange; import freemarker.template.expression.MethodCall; import freemarker.template.expression.Minus; import freemarker.template.expression.Modulo; import freemarker.template.expression.Multiply; import freemarker.template.expression.Not; import freemarker.template.expression.NotEquals; import freemarker.template.expression.NumberLiteral; import freemarker.template.expression.Or; import freemarker.template.expression.Plus; import freemarker.template.expression.StringLiteral; import freemarker.template.expression.Variable; import freemarker.template.instruction.AssignBlockInstruction; import freemarker.template.instruction.AssignInstruction; import freemarker.template.instruction.BreakInstruction; import freemarker.template.instruction.CallInstruction; import freemarker.template.instruction.CaseInstruction; import freemarker.template.instruction.CommentInstruction; import freemarker.template.instruction.ContainerInstruction; import freemarker.template.instruction.DefaultCaseInstruction; import freemarker.template.instruction.ElseInstruction; import freemarker.template.instruction.EndInstruction; import freemarker.template.instruction.ExitInstruction; import freemarker.template.instruction.FunctionInstruction; import freemarker.template.instruction.IfElseInstruction; import freemarker.template.instruction.IfInstruction; import freemarker.template.instruction.IncludeInstruction; import freemarker.template.instruction.Instruction; import freemarker.template.instruction.ListInstruction; import freemarker.template.instruction.NoParseInstruction; import freemarker.template.instruction.SwitchInstruction; import freemarker.template.instruction.TextBlockInstruction; import freemarker.template.instruction.TransformInstruction; import freemarker.template.instruction.VariableInstruction; /** * Parses standard template language and generates * {@link freemarker.template.instruction.Instruction}s. Uses * {@link freemarker.template.expression.ExpressionBuilder} to build * expressions. * * @version $Id: StandardTemplateParser.java 1189 2005-10-16 01:53:54Z run2000 $ */ public class StandardTemplateParser implements TemplateParser { /** The text to be parsed. */ protected String text; /** The number of characters in the text. */ protected int textLen; /** The Template being parsed. */ protected FunctionTemplateProcessor template; /** The current parse position. */ protected int parsePos = 0; /** The parse position before the current instruction was found. */ protected int previousParsePos = 0; /** The position at which the current instruction was found. */ protected int foundPos = 0; /** The next non-text instruction found by the parser. */ protected Instruction nextFMInstruction; /** * A Map of tag names to {@link Tag} objects, which are stored as flywheels. */ private static Map tagMap = initTags(); /** * A Map of two-character operator strings to {@link LongOperator} objects, * which are stored as flywheels. */ private static Map longOpMap = initLongOps(); // Template syntax protected static final int MAX_TAG_NAME_LENGTH = 10; protected static final String VAR_INSTR_START_CHARS = "${"; protected static final char VAR_INSTR_START_CHAR = '$'; protected static final char VAR_INSTR_END_CHAR = '}'; protected static final String LIST_TAG = "list"; protected static final String LIST_INDEX_KEYWORD = "as"; protected static final String LIST_END_TAG = "/list"; protected static final String IF_TAG = "if"; protected static final String ELSE_TAG = "else"; protected static final String ELSE_IF_TAG = "elseif"; protected static final String IF_END_TAG = "/if"; protected static final String SWITCH_TAG = "switch"; protected static final String SWITCH_END_TAG = "/switch"; protected static final String CASE_TAG = "case"; protected static final String BREAK_TAG = "break"; protected static final String DEFAULT_TAG = "default"; protected static final String ASSIGN_TAG = "assign"; protected static final String ASSIGN_END_TAG = "/assign"; protected static final String INCLUDE_TAG = "include"; protected static final String FUNCTION_TAG = "function"; protected static final String FUNCTION_END_TAG = "/function"; protected static final String CALL_TAG = "call"; protected static final String EXIT_TAG = "exit"; protected static final char TAG_START_CHAR = '<'; protected static final char TAG_END_CHAR = '>'; protected static final char END_TAG_START_CHAR = '/'; protected static final char QUOTE_CHAR = '\''; protected static final char DOUBLE_QUOTE_CHAR = '\"'; protected static final char ESCAPE_CHAR = '\\'; protected static final char BOOLEAN_ESCAPE_CHAR = '#'; protected static final char LIST_LITERAL_START_CHAR = '['; protected static final char LIST_LITERAL_END_CHAR = ']'; protected static final char HASH_LITERAL_START_CHAR = '{'; protected static final char HASH_LITERAL_END_CHAR = '}'; protected static final String LIST_LITERAL_RANGE = ".."; protected static final String COMMENT_TAG = "comment"; protected static final String COMMENT_END_TAG = "/comment"; protected static final String NOPARSE_TAG = "noparse"; protected static final String NOPARSE_TAG_END = "/noparse"; protected static final String FOREACH_TAG = "foreach"; protected static final String FOREACH_INDEX_KEYWORD = "in"; protected static final String FOREACH_END_TAG = "/foreach"; protected static final String TRANSFORM_TAG = "transform"; protected static final String TRANSFORM_END_TAG = "/transform"; protected static final String TRUE_LITERAL = "true"; protected static final String FALSE_LITERAL = "false"; protected static final String EMPTY_LITERAL = "empty"; protected static final String LOCAL_KEYWORD = "local"; protected static final String GLOBAL_KEYWORD = "global"; /** Length of operators that are more than one character long. */ protected static final int LONG_OPERATOR_LENGTH = 2; /** Default constructor. */ public StandardTemplateParser() { } /** * Constructs a new <code>StandardTemplateParser</code> with the given * template and text to be parsed. * * @param template * a new template that will received the parsed instructions * @param text * the text to be parsed * @deprecated use InputSources to pass in the text to be compiled where * possible */ public StandardTemplateParser(FunctionTemplateProcessor template, String text) { setTemplate(template); setText(text); } /** * Constructs a new <code>StandardTemplateParser</code> with the given * template and text to be parsed. * * @param template * a new template that will received the parsed instructions * @param source * the text to be parsed * @throws IOException * there was a problem reading the stream */ public StandardTemplateParser(FunctionTemplateProcessor template, InputSource source) throws IOException { String text = getTemplateText(source); setTemplate(template); setText(text); } /** * Takes the given Reader, reads it until the end of the stream, and * accumulates the contents in a String. The string is returned when the * stream is exhausted. * * @param source * the input stream to be turned into a String * @return a String representation of the given stream * @throws IOException * something went wrong while processing the stream */ protected static String getTemplateText(InputSource source) throws IOException { Reader reader = source.getReader(); if (reader == null) { InputStream stream = source.getInputStream(); if (stream == null) { throw new IllegalArgumentException("InputSource contains neither character nor byte stream"); } String encoding = source.getEncoding(); if (encoding == null) { reader = new InputStreamReader(stream); } else { reader = new InputStreamReader(stream, encoding); } } StringBuffer textBuf = new StringBuffer(); BufferedReader br = new BufferedReader(reader); char cbuf[] = new char[1024]; int nSize; try { nSize = br.read(cbuf); while (nSize > 0) { textBuf.append(cbuf, 0, nSize); nSize = br.read(cbuf); } } finally { try { br.close(); } catch (IOException e) { } } return textBuf.toString(); } /** * Initialize FM-Classic tag flywheel map. * * @return a <code>Map</code> of <code>Tag</code> flywheels. */ private static Map initTags() { Map tagMap = new HashMap(); // Map tag strings to Tag objects. tagMap.put(LIST_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseListStart(); } }); tagMap.put(LIST_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.LIST_END); } }); tagMap.put(IF_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseIfStart(); } }); tagMap.put(ELSE_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new ElseInstruction(); } }); tagMap.put(ELSE_IF_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseElseIf(); } }); tagMap.put(IF_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.IF_END); } }); tagMap.put(SWITCH_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseSwitch(); } }); tagMap.put(SWITCH_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.SWITCH_END); } }); tagMap.put(CASE_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseCase(); } }); tagMap.put(BREAK_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return BreakInstruction.getInstance(); } }); tagMap.put(DEFAULT_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new DefaultCaseInstruction(); } }); tagMap.put(ASSIGN_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseAssign(); } }); tagMap.put(INCLUDE_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseInclude(); } }); tagMap.put(FUNCTION_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseFunction(); } }); tagMap.put(FUNCTION_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.FUNCTION_END); } }); tagMap.put(CALL_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseCall(); } }); tagMap.put(EXIT_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return ExitInstruction.getInstance(); } }); tagMap.put(COMMENT_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return CommentInstruction.getInstance(); } }); tagMap.put(COMMENT_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.COMMENT_END); } }); tagMap.put(FOREACH_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseForeachStart(); } }); tagMap.put(FOREACH_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.FOREACH_END); } }); tagMap.put(NOPARSE_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new NoParseInstruction(); } }); tagMap.put(NOPARSE_TAG_END, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.NOPARSE_END); } }); tagMap.put(TRANSFORM_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return p.parseTransformStart(); } }); tagMap.put(TRANSFORM_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.TRANSFORM_END); } }); tagMap.put(ASSIGN_END_TAG, new Tag() { Instruction parse(StandardTemplateParser p) throws ParseException { return new EndInstruction(Instruction.ASSIGN_END); } }); return tagMap; } /** * Initialize long operator flywheel map. * * @return a <code>Map</code> of <code>LongOperator</code> flywheels. */ private static Map initLongOps() { Map longOpMap = new HashMap(); // Map long operator strings to LongOperator objects. longOpMap.put("==", new LongOperator() { Expression parse() throws ParseException { return new Equals(); } }); longOpMap.put("!=", new LongOperator() { Expression parse() throws ParseException { return new NotEquals(); } }); longOpMap.put("&&", new LongOperator() { Expression parse() throws ParseException { return new And(); } }); longOpMap.put("||", new LongOperator() { Expression parse() throws ParseException { return new Or(); } }); longOpMap.put("lt", new LongOperator() { Expression parse() throws ParseException { return new LessThan(); } }); longOpMap.put("le", new LongOperator() { Expression parse() throws ParseException { return new LessThanOrEquals(); } }); longOpMap.put("gt", new LongOperator() { Expression parse() throws ParseException { return new GreaterThan(); } }); longOpMap.put("ge", new LongOperator() { Expression parse() throws ParseException { return new GreaterThanOrEquals(); } }); longOpMap.put("eq", new LongOperator() { Expression parse() throws ParseException { return new Equals(); } }); longOpMap.put("ne", new LongOperator() { Expression parse() throws ParseException { return new NotEquals(); } }); longOpMap.put("or", new LongOperator() { Expression parse() throws ParseException { return new Or(); } }); return longOpMap; } /** * Sets the text to be parsed. * * @param text * the text to be parsed. */ public void setText(String text) { this.text = text; textLen = text.length(); } /** * Sets the template to receive the parsed instructions. * * @param template * the template being parsed. */ public void setTemplate(FunctionTemplateProcessor template) { this.template = template; } /** * <p> * Searches the text for an instruction, starting at the current parse * position. If one is found, parses it into an * {@link freemarker.template.instruction.Instruction}. Before changing * <code>parsePos</code>, sets <code>previousParsePos = parsePos</code>. * </p> * * <p> * If no instruction is found, leaves <code>parsePos</code> unchanged. * </p> * * @return a <code>Instruction</code> representing the next instruction * following <code>parsePos</code>, or <code>null</code> if none is * found. * @throws ParseException * the next instruction couldn't be parsed */ public Instruction getNextInstruction() throws ParseException { Instruction fmInstruction = nextFMInstruction; if (fmInstruction != null) { nextFMInstruction = null; return fmInstruction; } fmInstruction = getNextInstructionTag(); if (foundPos > previousParsePos) { nextFMInstruction = fmInstruction; String textBlock = text.substring(previousParsePos, foundPos); return new TextBlockInstruction(textBlock); } return fmInstruction; } /** * <p> * Searches the text for a tagged instruction, starting at the current parse * position. If one is found, parses it into a * {@link freemarker.template.instruction.Instruction}. Before changing * <code>parsePos</code>, sets <code>previousParsePos = parsePos</code>. * </p> * * <p> * If no instruction is found, <code>parsePos</code> will be equal to * <code>textLen</code>, which will be equal to <code>foundPos</code>. * </p> * * @return a <code>Instruction</code> representing the next instruction * following <code>parsePos</code>, or <code>null</code> if none is * found. * @throws ParseException * the next instruction couldn't be parsed */ protected Instruction getNextInstructionTag() throws ParseException { previousParsePos = parsePos; while (parsePos < textLen) { char c = text.charAt(parsePos); switch (c) { // If this is an HTML-style tag, get its name. case TAG_START_CHAR: int tagStartPos = parsePos; parsePos++; findTagNameEnd(); String tagName = text.substring(tagStartPos + 1, parsePos); // If we have a Tag object for this tag, // have the Tag object call us back to parse it. Tag tag = (Tag) tagMap.get(tagName); if (tag != null) { foundPos = tagStartPos; Instruction instruction = tag.parse(this); if (!findTagEnd()) { String errorMessage = "Syntax error" + atChar(foundPos); throw new ParseException(errorMessage); } return instruction; } else { parsePos = tagStartPos; } break; // If this is a variable instruction, parse it. case VAR_INSTR_START_CHAR: if (text.startsWith(VAR_INSTR_START_CHARS, parsePos)) { foundPos = parsePos; return parseVariableInstruction(); } } parsePos++; } foundPos = textLen; return null; } /** * Are there any more instructions left to be parsed? * * @return <code>true</code> if there is more text to parse, otherwise * <code>false</code> */ public boolean isMoreInstructions() { return (nextFMInstruction != null) || (parsePos < textLen); } /** * <p> * Searches the text for a matching end instruction, starting at the current * parse position. If we find it, parse it and return. Before changing * <code>parsePos</code>, should set <code>previousParsePos = * parsePos</code>. * </p> * * <p> * If no instruction is found, set <code>parsePos</code> to * <code>textLen</code> which equals <code>foundPos</code>. * </p> * * @return a <code>String</code> containing the intermediate text if we find * the end instruction we're after, otherwise <code>null</code> */ public String skipToEndInstruction(ContainerInstruction beginInstruction) { previousParsePos = parsePos; while (parsePos < textLen) { // If this is an HTML-style tag, get its name. if (text.charAt(parsePos) == TAG_START_CHAR) { int tagStartPos = parsePos; parsePos++; findTagNameEnd(); if (text.charAt(tagStartPos + 1) == END_TAG_START_CHAR) { String tagName = text.substring(tagStartPos + 1, parsePos); Tag tag = (Tag) tagMap.get(tagName); // If we have a Tag object for this tag, and it's an // end tag, have the Tag object call us back to parse it. if (tag != null) { foundPos = tagStartPos; try { Instruction endInstruction = tag.parse(this); if (beginInstruction.testEndInstruction(endInstruction)) { try { if (findTagEnd()) { return text.substring(previousParsePos, foundPos); } } catch (ParseException e) { // End of file encountered return null; } } } catch (ParseException e) { // Not the end tag we were looking for, keep // going... } } } parsePos = tagStartPos + 1; } else { parsePos++; } } foundPos = textLen; return null; } /** * Adds text to an error message indicating the line number where the error * was found. * * @return a <code>String</code> containing the message. */ public String atChar() { return atChar(foundPos); } /** * Advances <code>parsePos</code> through any remaining alphanumeric * characters. Leaves <code>parsePos</code> unchanged if not found. * * We check for the maximum length that a FM-Classic tag name could be. This * is a simple optimization to ensure that we don't end up with massively * long tag names being returned. Note that we intentionally go one past the * maximum length, to ensure we have a complete name. */ protected void findTagNameEnd() { int tagNameEndPos = parsePos; int length = 0; char c; if ((tagNameEndPos < textLen) && (text.charAt(tagNameEndPos) == END_TAG_START_CHAR)) { tagNameEndPos++; } while ((tagNameEndPos < textLen) && (length <= MAX_TAG_NAME_LENGTH)) { c = text.charAt(tagNameEndPos); if (!Character.isLetterOrDigit(c)) { parsePos = tagNameEndPos; return; } tagNameEndPos++; length++; } } /** * Requires a <code>TAG_END_CHAR</code>, optionally preceded by whitespace, * and advances <code>parsePos</code> after the <code>TAG_END_CHAR</code>. * * @return <code>true</code> if we found the end of the tag, otherwise * <code>false</code> * @throws ParseException * an error occurred while scanning for the end tag */ protected boolean findTagEnd() throws ParseException { if (skipToTagEnd()) { parsePos++; return true; } return false; } /** * Requires a <code>TAG_END_CHAR</code>, optionally preceded by whitespace, * and advances <code>parsePos</code> to the <code>TAG_END_CHAR</code>. * * @return <code>true</code> if we found the end of the tag, otherwise * <code>false</code> * @throws ParseException * an error occurred while scanning for the end tag */ protected boolean skipToTagEnd() throws ParseException { skipWhitespace(); return text.charAt(parsePos) == TAG_END_CHAR; } /** * Adds text to an error message indicating the line number where the error * was found. * * @return a <code>String</code> containing the message. */ protected String atChar(int pos) { String lineFeed = System.getProperty("line.separator"); char newline = lineFeed.charAt(lineFeed.length() - 1); int lineNum = 1; for (int charParsed = 0; charParsed < pos; charParsed++) { if (text.charAt(charParsed) == newline) { lineNum++; } } return " at line " + String.valueOf(lineNum); } // ----------------------------------------------------------------------- // Parse FreeMarker expressions // ----------------------------------------------------------------------- /** * Parses and builds an {@link freemarker.template.expression.Expression}, * which may also be a sub-expression. An expression in parenthesis is * considered a sub-expression. * * @return the completed <code>Expression</code>. */ protected Expression parseExpression() throws ParseException { int startPos = parsePos; // Tokenize the expression List tokens = parseElements(); if (tokens.size() == 0) { throw new ParseException("Missing expression" + atChar(startPos)); } // Build the tokens into an expression try { return ExpressionBuilder.buildExpression(tokens); } catch (ParseException e) { throw new ParseException("Syntax error in expression" + atChar(startPos), e); } } /** * Retrieve the next {@link freemarker.template.expression.Expression}(s) * following <code>parsePos</code>. * * @return a List of <code>Expression</code>s containing any elements * encountered * @throws ParseException * something went wrong during parsing */ protected List parseElements() throws ParseException { List elements = new LinkedList(); boolean moreExpected; do { skipWhitespace(); char c = text.charAt(parsePos); // Look for an identifier. if (isIdentifierStartChar(c)) { elements.add(parseVariable()); moreExpected = parseBinaryElement(elements); continue; } // Look for the things we can identify by one character: // a single-character operator, a parenthesis, a bracket, // or a string literal. switch (c) { case QUOTE_CHAR: case DOUBLE_QUOTE_CHAR: elements.add(parseStringLiteral()); moreExpected = parseBinaryElement(elements); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-': elements.add(parseNumberLiteral()); moreExpected = parseBinaryElement(elements); break; case '(': parsePos++; elements.add(parseExpression()); requireChar(')'); moreExpected = parseBinaryElement(elements); break; case '!': parsePos++; elements.add(new Not()); moreExpected = true; break; case LIST_LITERAL_START_CHAR: parsePos++; elements.add(parseListLiteral()); return elements; case HASH_LITERAL_START_CHAR: parsePos++; elements.add(parseHashLiteral()); return elements; case BOOLEAN_ESCAPE_CHAR: parsePos++; elements.add(parseBooleanLiteral()); moreExpected = parseBinaryElement(elements); break; default: moreExpected = false; } } while (moreExpected); return elements; } /** * Parse an optional binary element. If one is found, it is parsed and added * to the list of elements. * * @param elements * a list of elements to which we add any elements parsed * @return <code>true</code> if a binary operator was found, otherwise * <code>false</code> * @throws ParseException * something went wrong during parsing */ protected boolean parseBinaryElement(List elements) throws ParseException { skipWhitespace(); char c = text.charAt(parsePos); // Look for the things we can identify by one character: // a single-character operator, a parenthesis, a bracket, // or a string literal. switch (c) { case '+': parsePos++; elements.add(new Plus()); return true; case '-': parsePos++; elements.add(new Minus()); return true; case '/': parsePos++; elements.add(new Divide()); return true; case '*': parsePos++; elements.add(new Multiply()); return true; case '%': parsePos++; elements.add(new Modulo()); return true; } // Look for a long operator. if (parsePos + LONG_OPERATOR_LENGTH > textLen) { return false; } String possibleOpString = text.substring(parsePos, parsePos + LONG_OPERATOR_LENGTH); LongOperator longOp = (LongOperator) longOpMap.get(possibleOpString); // If we have a matching LongOperator object, ask it for a // corresponding Expression object, and return that object. if (longOp != null) { parsePos += LONG_OPERATOR_LENGTH; elements.add(longOp.parse()); return true; } // Check for long "and" operator if (parsePos + 3 > textLen) { return false; } possibleOpString = text.substring(parsePos, parsePos + 3); if ("and".equals(possibleOpString)) { parsePos += 3; elements.add(new And()); return true; } return false; } /** * Parses a {@link freemarker.template.expression.DynamicKeyName}. Expects * <code>parsePos</code> to be on the open bracket. * * @return a <code>DynamicKeyName</code>. */ protected DynamicKeyName parseDynamicKeyName() throws ParseException { int startPos = parsePos; Expression nameExpression = parseExpression(); if (text.charAt(parsePos) == ']') { parsePos++; return new DynamicKeyName(nameExpression); } else { throw new ParseException("Missing closing delimiter for key expression, " + "or illegal character in expression," + atChar(startPos)); } } /** * Parses the {@link freemarker.template.expression.Dot} operator. Expects * <code>parsePos</code> to be one character beyond the dot itself. * * @return a <code>Dot</code>. */ protected Dot parseDot() throws ParseException { return new Dot(parseIdentifier()); } /** * Parses a {@link freemarker.template.expression.StringLiteral}. Expects * <code>parsePos</code> to be on the open quote. This is so we can * determine whether a single- or double-quote was used to open the String, * and thus what close quote we should look for. * * @return a <code>StringLiteral</code>. */ protected Expression parseStringLiteral() throws ParseException { int startPos = parsePos; char quoteChar = text.charAt(parsePos); parsePos++; StringBuffer stringValueBuf = null; int prevPos = parsePos; boolean found = false; char currentChar; while (parsePos < textLen) { currentChar = text.charAt(parsePos); if (currentChar == quoteChar) { found = true; break; } else if (currentChar == ESCAPE_CHAR) { if (stringValueBuf == null) { stringValueBuf = new StringBuffer(); } stringValueBuf.append(text.substring(prevPos, parsePos)); parsePos++; // Skip over the escape character prevPos = parsePos; } parsePos++; } if (found) { String result; if (stringValueBuf != null) { if (parsePos > prevPos) { stringValueBuf.append(text.substring(prevPos, parsePos)); } result = stringValueBuf.toString(); } else { result = text.substring(prevPos, parsePos); } parsePos++; return new StringLiteral(result).resolveExpression(); } else { throw new ParseException("Unterminated string literal" + atChar(startPos)); } } /** * Parses a {@link freemarker.template.expression.ListLiteral}. Expects * <code>parsePos</code> to be just beyond open square bracket. * * @return a <code>ListLiteral</code>. */ protected Expression parseListLiteral() throws ParseException { int startPos = parsePos; try { if (skipChar(LIST_LITERAL_END_CHAR)) { return new ListLiteral(new ArrayList(0)).resolveExpression(); } Expression exp = parseExpression(); if (skipKeyword(LIST_LITERAL_RANGE)) { Expression exp2 = parseExpression(); requireChar(LIST_LITERAL_END_CHAR); return new ListRange(exp, exp2).resolveExpression(); } else { List arguments = new ArrayList(); arguments.add(exp); while (skipChar(',')) { arguments.add(parseExpression()); } requireChar(LIST_LITERAL_END_CHAR); return new ListLiteral(arguments).resolveExpression(); } } catch (IllegalArgumentException e) { String errorMessage = "List range cannot be constructed from these arguments" + atChar(startPos); throw new ParseException(errorMessage, e); } catch (ParseException e) { String errorMessage = "Syntax error in list literal" + atChar(startPos); throw new ParseException(errorMessage, e); } catch (TemplateException e) { String errorMessage = "Cannot evaluate constant list literal" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses a {@link freemarker.template.expression.HashLiteral}. Expects * <code>parsePos</code> to be just beyond the open brace. * * @return a <code>HashLiteral</code>. */ protected Expression parseHashLiteral() throws ParseException { int startPos = parsePos; try { if (skipChar(HASH_LITERAL_END_CHAR)) { return new HashLiteral(new ArrayList(0)).resolveExpression(); } List arguments = new ArrayList(); do { arguments.add(parseExpression()); if ((!skipChar(',')) && (!skipChar('='))) { break; } arguments.add(parseExpression()); } while (skipChar(',')); requireChar(HASH_LITERAL_END_CHAR); return new HashLiteral(arguments).resolveExpression(); } catch (IllegalArgumentException e) { String errorMessage = "Illegal argument list supplied to hash literal" + atChar(startPos); throw new ParseException(errorMessage, e); } catch (ParseException e) { String errorMessage = "Syntax error in hash literal" + atChar(startPos); throw new ParseException(errorMessage, e); } catch (TemplateException e) { String errorMessage = "Cannot evaluate constant hash literal" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses a {@link freemarker.template.expression.NumberLiteral}. Expects * <code>parsePos</code> to be on the first digit or optional minus sign. * * @return a <code>NumberLiteral</code>. */ protected Expression parseNumberLiteral() throws ParseException { int startPos = parsePos; char currentChar; // Deal with negative numbers if (text.charAt(parsePos) == '-') { parsePos++; } while (parsePos < textLen) { currentChar = text.charAt(parsePos); if (Character.isDigit(currentChar)) { parsePos++; } else { break; } } if (parsePos > startPos) { try { return new NumberLiteral(text.substring(startPos, parsePos)).resolveExpression(); } catch (NumberFormatException e) { throw new ParseException("Could not convert number literal" + atChar(startPos), e); } } else { throw new ParseException("Unterminated number literal " + atChar(startPos)); } } /** * Parses a {@link freemarker.template.expression.BooleanLiteral} or an * {@link freemarker.template.expression.EmptyLiteral}. Expects * <code>parsePos</code> to be immediately following the '#' symbol. * * @return a <code>BooleanLiteral</code> or <code>EmptyLiteral</code>. */ protected Expression parseBooleanLiteral() throws ParseException { int startPos = parsePos; while ((parsePos < textLen) && (Character.isLetter(text.charAt(parsePos)))) { parsePos++; } String value = text.substring(startPos, parsePos); if (value.equals(TRUE_LITERAL)) { return BooleanLiteral.TRUE; } else if (value.equals(FALSE_LITERAL)) { return BooleanLiteral.FALSE; } else if (value.equals(EMPTY_LITERAL)) { return EmptyLiteral.EMPTY; } else { throw new ParseException("Unknown literal encountered " + atChar(startPos)); } } /** * Parses an {@link freemarker.template.expression.Expression} and ensures * that it's a {@link freemarker.template.expression.Variable}. * * @return a <code>Variable</code>. */ protected Variable parseVariable() throws ParseException { int startPos = parsePos; // Tokenize the variable Variable element = parseIdentifier(); if (element == null) { throw new ParseException("Missing variable" + atChar(startPos)); } List tokens = new ArrayList(); tokens.add(element); while ((element = parseVariableElement()) != null) { tokens.add(element); } // Build the resulting tokens into a variable expression Variable variableExpression; try { variableExpression = ExpressionBuilder.buildVariable(tokens); } catch (ParseException e) { throw new ParseException("Syntax error in expression" + atChar(startPos), e); } if (variableExpression instanceof Variable) { return (Variable) variableExpression; } else { throw new ParseException("Variable expected" + atChar(startPos)); } } /** * Retrieve the next {@link freemarker.template.expression.Variable} * following <code>parsePos</code>, and ensure its a * {@link freemarker.template.expression.Variable}. * * @return a new <code>Variable</code>, or <code>null</code> if none is * found. */ protected Variable parseVariableElement() throws ParseException { skipWhitespace(); char c = text.charAt(parsePos); // Look for the things we can identify by one character: // a single-character operator, a parenthesis, a bracket, // or a dot. switch (c) { case '.': if (text.substring(parsePos, parsePos + 2).equals(LIST_LITERAL_RANGE)) { return null; } parsePos++; return parseDot(); case '(': parsePos++; return parseMethodCall(); case LIST_LITERAL_START_CHAR: parsePos++; return parseDynamicKeyName(); default: return null; } } /** * Tries to parse an {@link freemarker.template.expression.Identifier}. * Skips any whitespace prior to the identifier. * * @return an <code>Identifier</code>. */ protected Identifier parseIdentifier() throws ParseException { skipWhitespace(); int startPos = parsePos; if (!isIdentifierStartChar(text.charAt(startPos))) { throw new ParseException("Identifier expected" + atChar(startPos)); } while (parsePos < textLen) { char c = text.charAt(parsePos); if (Character.isLetterOrDigit(c) || c == '_') { parsePos++; continue; } if (c == '#') { // The '#' character is only allowed at the end of an // identifier parsePos++; } return (Identifier) new Identifier(text.substring(startPos, parsePos)).resolveExpression(); } throw new ParseException("Unexpected end of file"); } /** * <p> * Determines whether a character is legal at the start of an identifier. An * identifier is either something like a tag name, such as "foreach", or a * variable name, such as "myHash". * </p> * * <p> * In this implementation, an identifier can only start with either a letter * or an underscore. * </p> * * <p> * Note that this method does not affect identifiers inside a dynamic key * name. * </p> */ protected static boolean isIdentifierStartChar(char c) { return (Character.isLetter(c) || c == '_'); } /** * Parses a {@link freemarker.template.expression.MethodCall}. * * @return a new <code>MethodCall</code> object initialized with the * arguments. */ protected MethodCall parseMethodCall() throws ParseException { int startPos = parsePos; List arguments = new ArrayList(); try { while (true) { if (skipChar(')')) { break; } skipWhitespace(); arguments.add(parseExpression()); skipChar(','); } } catch (ParseException e) { String errorMessage = "Syntax error in MethodCall statement" + atChar(startPos); throw new ParseException(errorMessage, e); } return new MethodCall(arguments); } /** * Parses either a variable name or a list literal. This used by the * <list> and <foreach> tags. * * @return an Expression representing either a variable or a list literal * @throws ParseException * the next expression element couldn't be parsed */ protected Expression parseVariableOrList() throws ParseException { skipWhitespace(); char nextChar = text.charAt(parsePos); if (nextChar == LIST_LITERAL_START_CHAR) { parsePos++; return parseListLiteral(); } if (isIdentifierStartChar(nextChar)) { return parseVariable(); } throw new ParseException("Expected variable or list literal"); } // ----------------------------------------------------------------------- // Methods for parsing individual FreeMarker tags // ----------------------------------------------------------------------- /** * Parses a {@link freemarker.template.instruction.VariableInstruction}. * Expects <code>parsePos</code> to be at the beginning of the * <code>VAR_INSTR_START_CHARS</code>. * * @return a <code>VariableInstruction</code>. */ protected VariableInstruction parseVariableInstruction() throws ParseException { int startPos = parsePos; parsePos += VAR_INSTR_START_CHARS.length(); Expression expression = parseExpression(); if (text.charAt(parsePos) == VAR_INSTR_END_CHAR) { parsePos++; try { return new VariableInstruction(expression); } catch (IllegalArgumentException e) { String errorMessage = "Variable expression wasn't a scalar or number" + atChar(startPos); throw new ParseException(errorMessage, e); } } else { throw new ParseException("Missing closing delimiter for expression, " + "or illegal character in expression," + atChar(startPos)); } } /** * Parses a {@link freemarker.template.instruction.ListInstruction}'s start * tag. * * @return a <code>ListInstruction</code> initialized with the values from * the tag. */ protected ListInstruction parseListStart() throws ParseException { int startPos = parsePos; Expression listExpression; Identifier indexVariable; try { // Get the variable representing the object to be listed. listExpression = parseVariableOrList(); // Make sure the expression stopped at the list index keyword. if (!skipKeyword(LIST_INDEX_KEYWORD)) { throw new ParseException("Expected '" + LIST_INDEX_KEYWORD + '\''); } requireWhitespace(); // Get the index variable. indexVariable = parseIdentifier(); } catch (ParseException e) { String errorMessage = "Syntax error in list statement" + atChar(startPos); throw new ParseException(errorMessage, e); } try { return new ListInstruction(listExpression, indexVariable); } catch (IllegalArgumentException e) { String errorMessage = "List expression wasn't a list" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses a {@link freemarker.template.instruction.ListInstruction}'s start * tag with the "foreach" keyword. * * @return a <code>ListInstruction</code> initialized with the values from * the tag. */ protected ListInstruction parseForeachStart() throws ParseException { int startPos = parsePos; Expression listExpression; Identifier indexVariable; try { // Get the index variable. indexVariable = parseIdentifier(); // Make sure the expression stopped at the foreach index keyword. if (!skipKeyword(FOREACH_INDEX_KEYWORD)) { throw new ParseException("Expected '" + FOREACH_INDEX_KEYWORD + '\''); } requireWhitespace(); // Get the variable representing the object to be listed. listExpression = parseVariableOrList(); } catch (ParseException e) { String errorMessage = "Syntax error in list statement" + atChar(startPos); throw new ParseException(errorMessage, e); } // foreach and list are the same, so we just return a ListInstruction return new ListInstruction(listExpression, indexVariable); } /** * Parses an {@link freemarker.template.instruction.IfElseInstruction}'s * start tag. * * @return an <code>IfElseInstruction</code> initialized with the expression * in the tag. */ protected IfElseInstruction parseIfStart() throws ParseException { int startPos = parsePos; Expression condition; try { condition = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in if statement" + atChar(startPos); throw new ParseException(errorMessage, e); } return new IfElseInstruction(condition); } /** * Parses an {@link freemarker.template.instruction.IfInstruction} * <elseif> tag. * * @return an <code>IfInstruction</code> initialised with the expression in * the tag. */ protected IfInstruction parseElseIf() throws ParseException { int startPos = parsePos; Expression condition; try { condition = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in if statement" + atChar(startPos); throw new ParseException(errorMessage, e); } return new IfInstruction(condition); } /** * Parses an {@link freemarker.template.instruction.AssignInstruction} or * {@link freemarker.template.instruction.AssignBlockInstruction}'s tag. * Determines whether the assign is an expression assignment or a block * assignment and returns the appropriate instruction. * * @return an <code>AssignInstruction</code> or * <code>AssignBlockInstruction</code> initialized with the values * from the tag. */ protected Instruction parseAssign() throws ParseException { int startPos = parsePos; Variable variable; Expression value; try { // Get the variable to assign to. variable = parseVariable(); // Skip an optional equals sign. skipChar('='); // Are we a block instruction? if (skipToTagEnd()) { return new AssignBlockInstruction(variable); } // Get the variable or literal to be assigned. value = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in assignment" + atChar(startPos); throw new ParseException(errorMessage, e); } try { return new AssignInstruction(variable, value); } catch (IllegalArgumentException e) { String errorMessage = "Cannot assign variable to iterator variable" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses an {@link freemarker.template.instruction.IncludeInstruction}'s * tag. * * @return an <code>IncludeInstruction</code> initialized with the name in * the tag. */ protected IncludeInstruction parseInclude() throws ParseException { int startPos = parsePos; Expression templateName; Expression templateType = null; try { templateName = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in include statement" + atChar(startPos); throw new ParseException(errorMessage, e); } // Optional second argument: the type of template to include. skipChar(';'); skipWhitespace(); if (text.charAt(parsePos) == TAG_END_CHAR) { try { return new IncludeInstruction(template, templateName); } catch (IllegalArgumentException e) { String errorMessage = "Unexpected type for template name in include instruction" + atChar(startPos); throw new ParseException(errorMessage, e); } } try { skipKeyword("type"); requireChar('='); templateType = parseExpression(); } finally { try { return new IncludeInstruction(template, templateName, templateType); } catch (IllegalArgumentException e) { String errorMessage = "Unexpected type for template name in include instruction" + atChar(startPos); throw new ParseException(errorMessage, e); } } } /** * Parses a {@link freemarker.template.instruction.SwitchInstruction}'s tag. * * @return a <code>SwitchInstruction</code> initialized with the expression * in the tag. */ protected SwitchInstruction parseSwitch() throws ParseException { int startPos = parsePos; Expression testExpression; try { // Get the test variable. testExpression = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in switch statement" + atChar(startPos); throw new ParseException(errorMessage, e); } try { return new SwitchInstruction(testExpression); } catch (IllegalArgumentException e) { String errorMessage = "Switch expression was neither a scalar nor a number" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses a {@link freemarker.template.instruction.CaseInstruction}'s tag. * * @return a <code>CaseInstruction</code> initialized with the expression in * the tag. */ protected CaseInstruction parseCase() throws ParseException { int startPos = parsePos; Expression expression; try { // Get the expression. expression = parseExpression(); } catch (ParseException e) { String errorMessage = "Syntax error in case statement" + atChar(startPos); throw new ParseException(errorMessage, e); } try { return new CaseInstruction(expression); } catch (IllegalArgumentException e) { String errorMessage = "Illegal case expression in switch statement" + atChar(startPos); throw new ParseException(errorMessage, e); } } /** * Parses a {@link freemarker.template.instruction.FunctionInstruction}'s * tag. * * @return a <code>FunctionInstruction</code> intialized with the argument * names in the tag. */ protected FunctionInstruction parseFunction() throws ParseException { int startPos = parsePos; Identifier functionName; List argumentNames = new ArrayList(); boolean localScope = false; try { // Get the function's name. functionName = parseIdentifier(); // Parse argument names. requireChar('('); while (true) { if (skipChar(')')) { break; } argumentNames.add(parseIdentifier()); skipChar(','); } if (!skipToTagEnd()) { // Another keyword to parse -- global or local variable scope if (skipKeyword(LOCAL_KEYWORD)) { localScope = true; } else if (!skipKeyword(GLOBAL_KEYWORD)) { String errorMessage = "Incorrect keyword in function declaration" + atChar(startPos); throw new ParseException(errorMessage); } } } catch (ParseException e) { String errorMessage = "Syntax error in function declaration" + atChar(startPos); throw new ParseException(errorMessage, e); } return new FunctionInstruction(functionName, argumentNames, localScope); } /** * Parses a {@link freemarker.template.instruction.CallInstruction}'s tag. * This essentially looks like a method call, so we parse it the same way. * * @return a <code>CallInstruction</code> initialized with the arguments in * the tag. */ protected CallInstruction parseCall() throws ParseException { int startPos = parsePos; Variable functionCall; functionCall = parseVariable(); if (functionCall instanceof MethodCall) { return new CallInstruction((MethodCall) functionCall); } String errorMessage = "Syntax error in call statement" + atChar(startPos); throw new ParseException(errorMessage); } /** * Parses a {@link freemarker.template.instruction.TransformInstruction}'s * tag. This tag consists of one parameter: the variable representing the * {@link freemarker.template.TemplateTransformModel} to be used for the * transformation. * * @return a <code>TransformInstruction</code> */ protected TransformInstruction parseTransformStart() throws ParseException { Variable transformVariable; int startPos = parsePos; try { // Get the index variable. skipWhitespace(); // Get the variable representing the object to be listed. transformVariable = parseVariable(); } catch (ParseException e) { String errorMessage = "Syntax error in transform statement" + atChar(startPos); throw new ParseException(errorMessage, e); } return new TransformInstruction(transformVariable); } // ----------------------------------------------------------------------- // Methods for simple buffer management // ----------------------------------------------------------------------- /** * Advances beyond any whitespace; then, if the next character matches a * given character, advances beyond it and returns <code>true</code>, * otherwise returns <code>false</code>. * * @return <code>true</code> if the character was found, otherwise * <code>false</code> */ protected boolean skipChar(char c) throws ParseException { skipWhitespace(); if (text.charAt(parsePos) == c) { parsePos++; return true; } else { return false; } } /** * Requires a given character, optionally preceded by by whitespace. * * @throws ParseException * the required character couldn't be found */ protected void requireChar(char c) throws ParseException { if (!skipChar(c)) { throw new ParseException("Character " + c + "could not be found"); } } /** * Skip over a given keyword. Skips over any whitespace prior to the keyword * itself. * * @param keyword * the keyword to skip over * @return whether we found the keyword * @throws ParseException * there are no more characters before the keyword is expected. */ protected boolean skipKeyword(String keyword) throws ParseException { int size = keyword.length(); skipWhitespace(); int endIndex = parsePos + size; if (endIndex >= textLen) { return false; } if (!text.substring(parsePos, endIndex).equals(keyword)) { return false; } parsePos = endIndex; return true; } /** * Advances <code>parsePos</code> beyond any whitespace. * * @throws ParseException * there are no more characters after whitespace has been * skipped. */ protected void skipWhitespace() throws ParseException { while (parsePos < textLen) { if (!Character.isWhitespace(text.charAt(parsePos))) { return; } parsePos++; } throw new ParseException("Unexpected end of file"); } /** * Advances <code>parsePos</code> beyond required whitespace. * * @throws ParseException * there are no more characters after whitespace has been * skipped, or if no whitespace could be found. */ protected void requireWhitespace() throws ParseException { int startPos = parsePos; skipWhitespace(); if (parsePos == startPos) { throw new ParseException("Whitespace expected" + atChar(startPos)); } } /** * Returns a string representation of the object. * * @return a <code>String</code> representation of the object */ public String toString() { StringBuffer buffer = new StringBuffer(75); buffer.append("StandardTemplateParser, "); buffer.append(textLen); buffer.append(" characters to parse, "); buffer.append(textLen - parsePos); buffer.append(" remaining."); return buffer.toString(); } }