blob: 4808106dd693da7d0d7fb9a1e730682a90d40c73 [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 freemarker.template.TemplateException;
/**
* A subclass of {@link TemplateException} that says that an FTL expression has evaluated to {@code null} or it refers
* to something that doesn't exist. At least in FreeMarker 2.3.x these two cases aren't distinguished.
*/
public class InvalidReferenceException extends TemplateException {
static final InvalidReferenceException FAST_INSTANCE;
static {
Environment prevEnv = Environment.getCurrentEnvironment();
try {
Environment.setCurrentEnvironment(null);
FAST_INSTANCE = new InvalidReferenceException(
"Invalid reference. Details are unavilable, as this should have been handled by an FTL construct. "
+ "If it wasn't, that's problably a bug in FreeMarker.",
null);
} finally {
Environment.setCurrentEnvironment(prevEnv);
}
}
private static final Object[] TIP = {
"If the failing expression is known to be legally refer to something that's sometimes null or missing, "
+ "either specify a default value like myOptionalVar!myDefault, or use ",
"<#if myOptionalVar??>", "when-present", "<#else>", "when-missing", "</#if>",
". (These only cover the last step of the expression; to cover the whole expression, "
+ "use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??"
};
private static final Object[] TIP_MISSING_ASSIGNMENT_TARGET = {
"If the target variable is known to be legally null or missing sometimes, instead of something like ",
"<#assign x += 1>", ", you could write ", "<#if x??>", "<#assign x += 1>", "</#if>",
" or ", "<#assign x = (x!0) + 1>"
};
private static final String TIP_NO_DOLLAR =
"Variable references must not start with \"$\", unless the \"$\" is really part of the variable name.";
private static final String TIP_LAST_STEP_DOT =
"It's the step after the last dot that caused this error, not those before it.";
private static final String TIP_LAST_STEP_SQUARE_BRACKET =
"It's the final [] step that caused this error, not those before it.";
private static final String TIP_JSP_TAGLIBS =
"The \"JspTaglibs\" variable isn't a core FreeMarker feature; "
+ "it's only available when templates are invoked through freemarker.ext.servlet.FreemarkerServlet"
+ " (or other custom FreeMarker-JSP integration solution).";
/**
* Creates and invalid reference exception that contains no information about what was missing or null.
* As such, try to avoid this constructor.
*/
public InvalidReferenceException(Environment env) {
super("Invalid reference: The expression has evaluated to null or refers to something that doesn't exist.",
env);
}
/**
* Creates and invalid reference exception that contains no programmatically extractable information about the
* blamed expression. As such, try to avoid this constructor, unless need to raise this expression from outside
* the FreeMarker core.
*/
public InvalidReferenceException(String description, Environment env) {
super(description, env);
}
/**
* This is the recommended constructor, but it's only used internally, and has no backward compatibility guarantees.
*
* @param expression The expression that evaluates to missing or null. The last step of the expression should be
* the failing one, like in {@code goodStep.failingStep.furtherStep} it should only contain
* {@code goodStep.failingStep}.
*/
InvalidReferenceException(_ErrorDescriptionBuilder description, Environment env, Expression expression) {
super(null, env, expression, description);
}
/**
* Use this whenever possible, as it returns {@link #FAST_INSTANCE} instead of creating a new instance, when
* appropriate.
*/
static InvalidReferenceException getInstance(Expression blamed, Environment env) {
if (env != null && env.getFastInvalidReferenceExceptions()) {
return FAST_INSTANCE;
} else {
if (blamed != null) {
final _ErrorDescriptionBuilder errDescBuilder
= new _ErrorDescriptionBuilder("The following has evaluated to null or missing:").blame(blamed);
if (endsWithDollarVariable(blamed)) {
errDescBuilder.tips(TIP_NO_DOLLAR, TIP);
} else if (blamed instanceof Dot) {
final String rho = ((Dot) blamed).getRHO();
String nameFixTip = null;
if ("size".equals(rho)) {
nameFixTip = "To query the size of a collection or map use ?size, like myList?size";
} else if ("length".equals(rho)) {
nameFixTip = "To query the length of a string use ?length, like myString?size";
}
errDescBuilder.tips(
nameFixTip == null
? new Object[] { TIP_LAST_STEP_DOT, TIP }
: new Object[] { TIP_LAST_STEP_DOT, nameFixTip, TIP });
} else if (blamed instanceof DynamicKeyName) {
errDescBuilder.tips(TIP_LAST_STEP_SQUARE_BRACKET, TIP);
} else if (blamed instanceof Identifier
&& ((Identifier) blamed).getName().equals("JspTaglibs")) {
errDescBuilder.tips(TIP_JSP_TAGLIBS, TIP);
} else {
errDescBuilder.tip(TIP);
}
return new InvalidReferenceException(errDescBuilder, env, blamed);
} else {
return new InvalidReferenceException(env);
}
}
}
/**
* Used for assignments that use operators like {@code +=}, when the target variable was null/missing.
*/
static InvalidReferenceException getInstance(String missingAssignedVarName, String assignmentOperator,
Environment env) {
if (env != null && env.getFastInvalidReferenceExceptions()) {
return FAST_INSTANCE;
} else {
final _ErrorDescriptionBuilder errDescBuilder = new _ErrorDescriptionBuilder(
"The target variable of the assignment, ",
new _DelayedJQuote(missingAssignedVarName),
", was null or missing, but the \"",
assignmentOperator, "\" operator needs to get its value before assigning to it."
);
if (missingAssignedVarName.startsWith("$")) {
errDescBuilder.tips(TIP_NO_DOLLAR, TIP_MISSING_ASSIGNMENT_TARGET);
} else {
errDescBuilder.tip(TIP_MISSING_ASSIGNMENT_TARGET);
}
return new InvalidReferenceException(errDescBuilder, env, null);
}
}
private static boolean endsWithDollarVariable(Expression blame) {
return blame instanceof Identifier && ((Identifier) blame).getName().startsWith("$")
|| blame instanceof Dot && ((Dot) blame).getRHO().startsWith("$");
}
}