blob: 7540d5277f1a93848c1d8825471a2b744d6cb16c [file] [log] [blame]
/*
* 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.HashSet;
import java.util.Iterator;
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 {
/**
* The version identifier for this Serializable class.
* Increment only if the <i>serialized</i> form of the
* class changes.
*/
private static final long serialVersionUID = 1L;
/**
* 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: &lt;result of getMessage&gt;
*/
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, (Template) null, lineNumber, columnNumber, null);
}
/**
* @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int)} instead.
* @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);
}
/**
* @deprecated Use {@link #ParseException(String, UnboundTemplate, int, int, int, int, Throwable)} instead.
* @since 2.3.21
*/
@Deprecated
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, UnboundTemplate, 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, UnboundTemplate, 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);
}
/**
* @deprecated Use {@link #ParseException(String, UnboundTemplate, Token)} instead.
* @since 2.3.20
*/
@Deprecated
public ParseException(String description, Template template, Token tk) {
this(description, template, tk, null);
}
/**
* @deprecated Use {@link #ParseException(String, UnboundTemplate, Token, Throwable)} instead.
* @since 2.3.20
*/
@Deprecated
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.4.0
*/
public ParseException(String description, UnboundTemplate unboundTemplate,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
this(description, unboundTemplate, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);
}
/**
* @since 2.4.0
*/
public ParseException(String description, UnboundTemplate unboundTemplate,
int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
Throwable cause) {
this(description,
unboundTemplate == null ? null : unboundTemplate.getSourceName(),
lineNumber, columnNumber,
endLineNumber, endColumnNumber,
cause);
}
/**
* @since 2.4.0
*/
public ParseException(String description, UnboundTemplate unboundTemplate, Token tk) {
this(description, unboundTemplate, tk, null);
}
/**
* @since 2.4.0
*/
public ParseException(String description, UnboundTemplate unboundTemplate, Token tk, Throwable cause) {
this(description,
unboundTemplate == null ? null : unboundTemplate.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.getUnboundTemplate() == null ? null : tobj.getUnboundTemplate().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
}
String tokenErrDesc;
if (currentToken != null) {
tokenErrDesc = getCustomTokenErrorDescription();
if (tokenErrDesc == null) {
// The default JavaCC message generation stuff follows.
StringBuilder expected = new StringBuilder();
int maxSize = 0;
for (int i = 0; i < expectedTokenSequences.length; i++) {
if (i != 0) {
expected.append(eol);
}
expected.append(" ");
if (maxSize < expectedTokenSequences[i].length) {
maxSize = expectedTokenSequences[i].length;
}
for (int j = 0; j < expectedTokenSequences[i].length; j++) {
if (j != 0) expected.append(' ');
expected.append(tokenImage[expectedTokenSequences[i][j]]);
}
}
tokenErrDesc = "Encountered \"";
Token tok = currentToken.next;
for (int i = 0; i < maxSize; i++) {
if (i != 0) tokenErrDesc += " ";
if (tok.kind == 0) {
tokenErrDesc += tokenImage[0];
break;
}
tokenErrDesc += add_escapes(tok.image);
tok = tok.next;
}
tokenErrDesc += "\", but ";
if (expectedTokenSequences.length == 1) {
tokenErrDesc += "was expecting:" + eol;
} else {
tokenErrDesc += "was expecting one of:" + eol;
}
tokenErrDesc += expected;
}
} else {
tokenErrDesc = null;
}
return tokenErrDesc;
}
private String getCustomTokenErrorDescription() {
final Token nextToken = currentToken.next;
final int kind = nextToken.kind;
if (kind == EOF) {
Set/*<String>*/ endNames = new HashSet();
for (int i = 0; i < expectedTokenSequences.length; i++) {
int[] sequence = expectedTokenSequences[i];
for (int j = 0; j < sequence.length; j++) {
switch (sequence[j]) {
case END_FOREACH:
endNames.add( "#foreach");
break;
case END_LIST:
endNames.add( "#list");
break;
case END_SWITCH:
endNames.add( "#switch");
break;
case END_IF:
endNames.add( "#if");
break;
case END_COMPRESS:
endNames.add( "#compress");
break;
case END_MACRO:
endNames.add( "#macro");
case END_FUNCTION:
endNames.add( "#function");
break;
case END_TRANSFORM:
endNames.add( "#transform");
break;
case END_ESCAPE:
endNames.add( "#escape");
break;
case END_NOESCAPE:
endNames.add( "#noescape");
break;
case END_ASSIGN:
endNames.add( "#assign");
break;
case END_LOCAL:
endNames.add( "#local");
break;
case END_GLOBAL:
endNames.add( "#global");
break;
case END_ATTEMPT:
endNames.add( "#attempt");
break;
case CLOSING_CURLY_BRACKET:
endNames.add( "\"{\"");
break;
case CLOSE_BRACKET:
endNames.add( "\"[\"");
break;
case CLOSE_PAREN:
endNames.add( "\"(\"");
break;
case UNIFIED_CALL_END:
endNames.add( "@...");
break;
}
}
}
return "Unexpected end of file reached."
+ (endNames.size() == 0 ? "" : " You have an unclosed " + concatWithOrs(endNames) + ".");
} else if (kind == ELSE) {
return "Unexpected directive, \"#else\". "
+ "Check if you have a valid #if-#elseif-#else or #list-#else structure.";
} else if (kind == END_IF || kind == ELSE_IF) {
return "Unexpected directive, "
+ StringUtil.jQuote(nextToken)
+ ". Check if you have a valid #if-#elseif-#else structure.";
}
return null;
}
private String concatWithOrs(Set/*<String>*/ endNames) {
StringBuilder sb = new StringBuilder();
for (Iterator/*<String>*/ it = endNames.iterator(); it.hasNext(); ) {
String endName = (String) it.next();
if (sb.length() != 0) {
sb.append(" or ");
}
sb.append(endName);
}
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();
}
}