freemarker.core.ParseException Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of freemarker-gae Show documentation
Show all versions of freemarker-gae Show documentation
Google App Engine compliant variation of FreeMarker.
FreeMarker is a "template engine"; a generic tool to generate text output based on templates.
/*
* 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 freemarker.core;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.utility.SecurityUtilities;
import freemarker.template.utility.StringUtil;
/**
* Parsing-time exception in a template (as opposed to a runtime exception, a {@link TemplateException}). This usually
* signals syntactical/lexical errors.
*
* Note that on JavaCC-level lexical errors throw {@link TokenMgrError} instead of this, however with the API-s that
* most users use those will be wrapped into {@link ParseException}-s.
*
* This is a modified version of file generated by JavaCC from FTL.jj.
* You can modify this class to customize the error reporting mechanisms so long as the public interface
* remains compatible with the original.
*
* @see TokenMgrError
*/
public class ParseException extends IOException implements FMParserConstants {
private static final String END_TAG_SYNTAX_HINT
= "(Note that FreeMarker end-tags must have # or @ after the / character.)";
/**
* This is the last token that has been consumed successfully. If
* this object has been created due to a parse error, the token
* following this token will (therefore) be the first error token.
*/
public Token currentToken;
private static volatile Boolean jbossToolsMode;
private boolean messageAndDescriptionRendered;
private String message;
private String description;
public int columnNumber, lineNumber;
public int endColumnNumber, endLineNumber;
/**
* Each entry in this array is an array of integers. Each array
* of integers represents a sequence of tokens (by their ordinal
* values) that is expected at this point of the parse.
*/
public int[][] expectedTokenSequences;
/**
* This is a reference to the "tokenImage" array of the generated
* parser within which the parse error occurred. This array is
* defined in the generated ...Constants interface.
*/
public String[] tokenImage;
/**
* The end of line string for this machine.
*/
protected String eol = SecurityUtilities.getSystemProperty("line.separator", "\n");
/** @deprecated Will be remove without replacement in 2.4. */
@Deprecated
protected boolean specialConstructor;
private String templateName;
/**
* This constructor is used by the method "generateParseException"
* in the generated parser. Calling this constructor generates
* a new object of this type with the fields "currentToken",
* "expectedTokenSequences", and "tokenImage" set.
* This constructor calls its super class with the empty string
* to force the "toString" method of parent class "Throwable" to
* print the error message in the form:
* ParseException: <result of getMessage>
*/
public ParseException(Token currentTokenVal,
int[][] expectedTokenSequencesVal,
String[] tokenImageVal
) {
super("");
currentToken = currentTokenVal;
specialConstructor = true;
expectedTokenSequences = expectedTokenSequencesVal;
tokenImage = tokenImageVal;
lineNumber = currentToken.next.beginLine;
columnNumber = currentToken.next.beginColumn;
endLineNumber = currentToken.next.endLine;
endColumnNumber = currentToken.next.endColumn;
}
/**
* The following constructors are for use by you for whatever
* purpose you can think of. Constructing the exception in this
* manner makes the exception behave in the normal way - i.e., as
* documented in the class "Throwable". The fields "errorToken",
* "expectedTokenSequences", and "tokenImage" do not contain
* relevant information. The JavaCC generated code does not use
* these constructors.
*
* @deprecated Use a constructor to which you pass description, template, and positions.
*/
@Deprecated
protected ParseException() {
super();
}
/**
* @deprecated Use a constructor to which you can also pass the template, and the end positions.
*/
@Deprecated
public ParseException(String description, int lineNumber, int columnNumber) {
this(description, null, lineNumber, columnNumber, null);
}
/**
* @since 2.3.21
*/
public ParseException(String description, Template template,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
this(description, template, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);
}
/**
* @since 2.3.21
*/
public ParseException(String description, Template template,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
Throwable cause) {
this(description,
template == null ? null : template.getSourceName(),
lineNumber, columnNumber,
endLineNumber, endColumnNumber,
cause);
}
/**
* @deprecated Use {@link #ParseException(String, Template, int, int, int, int)} instead, as IDE-s need the end
* position of the error too.
* @since 2.3.20
*/
@Deprecated
public ParseException(String description, Template template, int lineNumber, int columnNumber) {
this(description, template, lineNumber, columnNumber, null);
}
/**
* @deprecated Use {@link #ParseException(String, Template, int, int, int, int, Throwable)} instead, as IDE-s need
* the end position of the error too.
* @since 2.3.20
*/
@Deprecated
public ParseException(String description, Template template, int lineNumber, int columnNumber, Throwable cause) {
this(description,
template == null ? null : template.getSourceName(),
lineNumber, columnNumber,
0, 0,
cause);
}
/**
* @since 2.3.20
*/
public ParseException(String description, Template template, Token tk) {
this(description, template, tk, null);
}
/**
* @since 2.3.20
*/
public ParseException(String description, Template template, Token tk, Throwable cause) {
this(description,
template == null ? null : template.getSourceName(),
tk.beginLine, tk.beginColumn,
tk.endLine, tk.endColumn,
cause);
}
/**
* @since 2.3.20
*/
public ParseException(String description, TemplateObject tobj) {
this(description, tobj, null);
}
/**
* @since 2.3.20
*/
public ParseException(String description, TemplateObject tobj, Throwable cause) {
this(description,
tobj.getTemplate() == null ? null : tobj.getTemplate().getSourceName(),
tobj.beginLine, tobj.beginColumn,
tobj.endLine, tobj.endColumn,
cause);
}
private ParseException(String description, String templateName,
int lineNumber, int columnNumber,
int endLineNumber, int endColumnNumber,
Throwable cause) {
super(description); // but we override getMessage, so it will be different
try {
this.initCause(cause);
} catch (Exception e) {
// Suppressed; we can't do more
}
this.description = description;
this.templateName = templateName;
this.lineNumber = lineNumber;
this.columnNumber = columnNumber;
this.endLineNumber = endLineNumber;
this.endColumnNumber = endColumnNumber;
}
/**
* Should be used internally only; sets the name of the template that contains the error.
* This is needed as the constructor that JavaCC automatically calls doesn't pass in the template, so we
* set it somewhere later in an exception handler.
*/
public void setTemplateName(String templateName) {
this.templateName = templateName;
synchronized (this) {
messageAndDescriptionRendered = false;
message = null;
}
}
/**
* Returns the error location plus the error description.
*
* @see #getDescription()
* @see #getTemplateName()
* @see #getLineNumber()
* @see #getColumnNumber()
*/
@Override
public String getMessage() {
synchronized (this) {
if (messageAndDescriptionRendered) return message;
}
renderMessageAndDescription();
synchronized (this) {
return message;
}
}
private String getDescription() {
synchronized (this) {
if (messageAndDescriptionRendered) return description;
}
renderMessageAndDescription();
synchronized (this) {
return description;
}
}
/**
* Returns the description of the error without error location or source quotations, or {@code null} if there's no
* description available. This is useful in editors (IDE-s) where the error markers and the editor window itself
* already carry this information, so it's redundant the repeat in the error dialog.
*/
public String getEditorMessage() {
return getDescription();
}
/**
* Returns the name (template-root relative path) of the template whose parsing was failed.
* Maybe {@code null} if this is a non-stored template.
*
* @since 2.3.20
*/
public String getTemplateName() {
return templateName;
}
/**
* 1-based line number of the failing section, or 0 is the information is not available.
*/
public int getLineNumber() {
return lineNumber;
}
/**
* 1-based column number of the failing section, or 0 is the information is not available.
*/
public int getColumnNumber() {
return columnNumber;
}
/**
* 1-based line number of the last line that contains the failing section, or 0 if the information is not available.
*
* @since 2.3.21
*/
public int getEndLineNumber() {
return endLineNumber;
}
/**
* 1-based column number of the last character of the failing section, or 0 if the information is not available.
* Note that unlike with Java string API-s, this column number is inclusive.
*
* @since 2.3.21
*/
public int getEndColumnNumber() {
return endColumnNumber;
}
private void renderMessageAndDescription() {
String desc = getOrRenderDescription();
String prefix;
if (!isInJBossToolsMode()) {
prefix = "Syntax error "
+ _MessageUtil.formatLocationForSimpleParsingError(templateName, lineNumber, columnNumber)
+ ":\n";
} else {
prefix = "[col. " + columnNumber + "] ";
}
String msg = prefix + desc;
desc = msg.substring(prefix.length()); // so we reuse the backing char[]
synchronized (this) {
message = msg;
description = desc;
messageAndDescriptionRendered = true;
}
}
private boolean isInJBossToolsMode() {
if (jbossToolsMode == null) {
try {
jbossToolsMode = Boolean.valueOf(
ParseException.class.getClassLoader().toString().indexOf(
"[org.jboss.ide.eclipse.freemarker:") != -1);
} catch (Throwable e) {
jbossToolsMode = Boolean.FALSE;
}
}
return jbossToolsMode.booleanValue();
}
/**
* Returns the description of the error without the error location, or {@code null} if there's no description
* available.
*/
private String getOrRenderDescription() {
synchronized (this) {
if (description != null) return description; // When we already have it from the constructor
}
if (currentToken == null) {
return null;
}
Token unexpectedTok = currentToken.next;
if (unexpectedTok.kind == EOF) {
Set endTokenDescs = getExpectedEndTokenDescs();
return "Unexpected end of file reached."
+ (endTokenDescs.size() == 0
? ""
: " You have an unclosed " + joinWithAnds(endTokenDescs)
+ ". Check if the FreeMarker end-tags are present, and aren't malformed. "
+ END_TAG_SYNTAX_HINT);
}
int maxExpectedTokenSequenceLength = 0;
for (int i = 0; i < expectedTokenSequences.length; i++) {
int[] expectedTokenSequence = expectedTokenSequences[i];
if (maxExpectedTokenSequenceLength < expectedTokenSequence.length) {
maxExpectedTokenSequenceLength = expectedTokenSequence.length;
}
}
StringBuilder tokenErrDesc = new StringBuilder();
tokenErrDesc.append("Encountered ");
boolean encounteredEndTag = false;
for (int i = 0; i < maxExpectedTokenSequenceLength; i++) {
if (i != 0) {
tokenErrDesc.append(" ");
}
if (unexpectedTok.kind == 0) {
tokenErrDesc.append(tokenImage[0]);
break;
}
String image = unexpectedTok.image;
if (i == 0) {
if (image.startsWith("") || image.startsWith("[/")) {
encounteredEndTag = true;
}
}
tokenErrDesc.append(StringUtil.jQuote(image));
unexpectedTok = unexpectedTok.next;
}
Set expectedEndTokenDescs;
int unexpTokKind = currentToken.next.kind;
if (getIsEndToken(unexpTokKind) || unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
expectedEndTokenDescs = new LinkedHashSet<>(getExpectedEndTokenDescs());
if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
// If <\#if> was expected, yet #else or #elseif wasn't, then this isn't nesting related problem.
expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(END_IF));
} else {
expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(unexpTokKind));
}
} else {
expectedEndTokenDescs = Collections.emptySet();
}
// Generate more helpful error message if this was a nesting related problem:
if (!expectedEndTokenDescs.isEmpty()) {
if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
tokenErrDesc.append(", which can only be used where an #if");
if (unexpTokKind == ELSE) {
tokenErrDesc.append(" or #list");
}
tokenErrDesc.append(" could be closed");
}
tokenErrDesc.append(", but at this place only ");
tokenErrDesc.append(expectedEndTokenDescs.size() > 1 ? "these" : "this");
tokenErrDesc.append(" can be closed: ");
boolean first = true;
for (String expectedEndTokenDesc : expectedEndTokenDescs) {
if (!first) {
tokenErrDesc.append(", ");
} else {
first = false;
}
tokenErrDesc.append(
!expectedEndTokenDesc.startsWith("\"")
? StringUtil.jQuote(expectedEndTokenDesc)
: expectedEndTokenDesc);
}
tokenErrDesc.append(".");
if (encounteredEndTag) {
tokenErrDesc.append(" This usually because of wrong nesting of FreeMarker directives, like a "
+ "missed or malformed end-tag somewhere. " + END_TAG_SYNTAX_HINT);
}
tokenErrDesc.append(eol);
tokenErrDesc.append("Was ");
} else {
tokenErrDesc.append(", but was ");
}
if (expectedTokenSequences.length == 1) {
tokenErrDesc.append("expecting pattern:");
} else {
tokenErrDesc.append("expecting one of these patterns:");
}
tokenErrDesc.append(eol);
for (int i = 0; i < expectedTokenSequences.length; i++) {
if (i != 0) {
tokenErrDesc.append(eol);
}
tokenErrDesc.append(" ");
int[] expectedTokenSequence = expectedTokenSequences[i];
for (int j = 0; j < expectedTokenSequence.length; j++) {
if (j != 0) {
tokenErrDesc.append(' ');
}
tokenErrDesc.append(tokenImage[expectedTokenSequence[j]]);
}
}
return tokenErrDesc.toString();
}
/**
* Returns the descriptions end-tags (or expression closing tokens) that we could have at this point.
* This is for generating error messages.
*/
private Set getExpectedEndTokenDescs() {
Set endTokenDescs = new LinkedHashSet<>();
for (int i = 0; i < expectedTokenSequences.length; i++) {
int[] sequence = expectedTokenSequences[i];
for (int j = 0; j < sequence.length; j++) {
int token = sequence[j];
String endTokenDesc = getEndTokenDescIfIsEndToken(token);
if (endTokenDesc != null) {
endTokenDescs.add(endTokenDesc);
}
}
}
return endTokenDescs;
}
private boolean getIsEndToken(int token) {
return getEndTokenDescIfIsEndToken(token) != null;
}
private String getEndTokenDescIfIsEndToken(int token) {
String endTokenDesc = null;
switch (token) {
case END_FOREACH:
endTokenDesc = "#foreach";
break;
case END_LIST:
endTokenDesc = "#list";
break;
case END_SEP:
endTokenDesc = "#sep";
break;
case END_ITEMS:
endTokenDesc = "#items";
break;
case END_SWITCH:
endTokenDesc = "#switch";
break;
case END_IF:
endTokenDesc = "#if";
break;
case END_COMPRESS:
endTokenDesc = "#compress";
break;
case END_MACRO:
case END_FUNCTION:
endTokenDesc = "#macro or #function";
break;
case END_TRANSFORM:
endTokenDesc = "#transform";
break;
case END_ESCAPE:
endTokenDesc = "#escape";
break;
case END_NOESCAPE:
endTokenDesc = "#noescape";
break;
case END_ASSIGN:
case END_GLOBAL:
case END_LOCAL:
endTokenDesc = "#assign or #local or #global";
break;
case END_ATTEMPT:
endTokenDesc = "#attempt";
break;
case CLOSING_CURLY_BRACKET:
endTokenDesc = "\"{\"";
break;
case CLOSE_BRACKET:
endTokenDesc = "\"[\"";
break;
case CLOSE_PAREN:
endTokenDesc = "\"(\"";
break;
case UNIFIED_CALL_END:
endTokenDesc = "@...";
break;
}
return endTokenDesc;
}
private String joinWithAnds(Collection strings) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
if (sb.length() != 0) {
sb.append(" and ");
}
sb.append(s);
}
return sb.toString();
}
/**
* Used to convert raw characters to their escaped version
* when these raw version cannot be used as part of an ASCII
* string literal.
*/
protected String add_escapes(String str) {
StringBuilder retval = new StringBuilder();
char ch;
for (int i = 0; i < str.length(); i++) {
switch (str.charAt(i))
{
case 0 :
continue;
case '\b':
retval.append("\\b");
continue;
case '\t':
retval.append("\\t");
continue;
case '\n':
retval.append("\\n");
continue;
case '\f':
retval.append("\\f");
continue;
case '\r':
retval.append("\\r");
continue;
case '\"':
retval.append("\\\"");
continue;
case '\'':
retval.append("\\\'");
continue;
case '\\':
retval.append("\\\\");
continue;
default:
if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
String s = "0000" + Integer.toString(ch, 16);
retval.append("\\u" + s.substring(s.length() - 4, s.length()));
} else {
retval.append(ch);
}
continue;
}
}
return retval.toString();
}
}