Added Compiler.nullValue() for situations where one wants to provide a value
for use when variables resolve to null, but still wishes for exceptions to be thrown when values are not resolvable (i.e. are typos). Pre-existing behavior is preserved if one uses defaultValue() or does not change the default configuration. Nota bene: there are complexities, so be sure to read the documentation.
This commit is contained in:
@@ -105,6 +105,7 @@ Sections behave as you would expect:
|
||||
|
||||
* `Boolean` values enable or disable the section.
|
||||
* Array, `Iterator`, or `Iterable` values repeatedly execute the section with each element used as the context for each iteration. Empty collections result in zero instances of the section being included in the template.
|
||||
* An unresolvable or null value is treated as false (by default, see //Default Values// for more details).
|
||||
* Any other object results in a single execution of the section with that object as a context.
|
||||
|
||||
See the code in
|
||||
@@ -134,9 +135,9 @@ the template.
|
||||
Default Values
|
||||
--------------
|
||||
|
||||
By default, an exception will be thrown any time a variable cannot be found, or
|
||||
resolves to null. A value to use in such circumstances can be provided when
|
||||
creating your compiler:
|
||||
By default, an exception will be thrown any time a variable cannot be resolved,
|
||||
or resolves to null. You can change this behavior in two ways. If you want to
|
||||
provide a value for use in all such circumstances, use `defaultValue()`:
|
||||
|
||||
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
|
||||
Mustache.compiler().defaultValue("what").compile(tmpl).execute(new Object() {
|
||||
@@ -147,6 +148,33 @@ creating your compiler:
|
||||
// result:
|
||||
Say what what?
|
||||
|
||||
If you only wish to provide a default value for variables that resolve to null,
|
||||
and wish to preserve exceptions in cases where variables cannot be resolved,
|
||||
use `nullValue()`:
|
||||
|
||||
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
|
||||
Mustache.compiler().nullValue("what").compile(tmpl).execute(new Object() {
|
||||
String exists = "Say";
|
||||
String nullValued = null;
|
||||
// String doesNotExist
|
||||
});
|
||||
// throws MustacheException when executing the template because
|
||||
// doesNotExist cannot be resolved
|
||||
|
||||
Note that any variable resolved against a `Map` context will be resolvable, but
|
||||
will be treated as having the value null if the map contains no mapping for the
|
||||
variable. Only variables resolved against Java object fields or methods risk
|
||||
being unresolvable.
|
||||
|
||||
Note that section behavior deviates from the above specification (for
|
||||
historical reasons and because it's kind of useful). By default, a section that
|
||||
is not resolvable or resolves to null will be omitted (and conversely, an
|
||||
inverse section that is not resolvable or resolves to null will be included).
|
||||
If you use `defaultValue()`, this behavior is preserved. If you use
|
||||
`nullValue()`, sections that refer to an unresolvable variable will now throw
|
||||
an exception (sections that refer to a resolvable, but null-valued variable,
|
||||
will behave as before).
|
||||
|
||||
Extensions
|
||||
==========
|
||||
|
||||
|
||||
@@ -38,9 +38,15 @@ public class Mustache
|
||||
/** Whether or not standards mode is enabled. */
|
||||
public final boolean standardsMode;
|
||||
|
||||
/** A value to use when a variable cannot be resolved, or resolves to null. If the default
|
||||
* value is null (which is the default default value), an exception will be thrown. */
|
||||
public final String defaultValue;
|
||||
/** A value to use when a variable resolves to null. If this value is null (which is the
|
||||
* default null value), an exception will be thrown. If {@link #missingIsNull} is also
|
||||
* true, this value will be used when a variable cannot be resolved. */
|
||||
public final String nullValue;
|
||||
|
||||
/** If this value is true, missing variables will be treated like variables that return
|
||||
* null. {@link #nullValue} will be used in their place, assuming {@link #nullValue} is
|
||||
* configured to a non-null value. */
|
||||
public final boolean missingIsNull;
|
||||
|
||||
/** The template loader in use during this compilation. */
|
||||
public final TemplateLoader loader;
|
||||
@@ -57,32 +63,59 @@ public class Mustache
|
||||
|
||||
/** Returns a compiler that either does or does not escape HTML by default. */
|
||||
public Compiler escapeHTML (boolean escapeHTML) {
|
||||
return new Compiler(escapeHTML, this.standardsMode, this.defaultValue, this.loader);
|
||||
return new Compiler(escapeHTML, this.standardsMode,
|
||||
this.nullValue, this.missingIsNull, this.loader);
|
||||
}
|
||||
|
||||
/** Returns a compiler that either does or does not use standards mode. Standards mode
|
||||
* disables the non-standard JMustache extensions like looking up missing names in a parent
|
||||
* context. */
|
||||
public Compiler standardsMode (boolean standardsMode) {
|
||||
return new Compiler(this.escapeHTML, standardsMode, this.defaultValue, this.loader);
|
||||
return new Compiler(this.escapeHTML, standardsMode,
|
||||
this.nullValue, this.missingIsNull, this.loader);
|
||||
}
|
||||
|
||||
/** Returns a compiler that will use the given value for any variable that is missing, or
|
||||
* otherwise resolves to null. */
|
||||
* otherwise resolves to null. This is like {@link #nullValue} except that it returns the
|
||||
* supplied default for missing keys and existing keys that return null values. Note that
|
||||
* regardless of whether a null or default value is configured, if the resolution of part
|
||||
* of a compound key results in a missing or null value, an exception will be raised. */
|
||||
public Compiler defaultValue (String defaultValue) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode, defaultValue, this.loader);
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
defaultValue, true, this.loader);
|
||||
}
|
||||
|
||||
/** Returns a compiler that will use the given value for any variable that resolves to
|
||||
* null, but will still raise an exception for variables for which an accessor cannot be
|
||||
* found. This is like {@link #defaultValue} except that it differentiates between missing
|
||||
* accessors, and accessors that exist but return null.
|
||||
* <ul>
|
||||
* <li>In the case of a Java object being used as a context, if no field or method can be
|
||||
* found for a variable, an exception will be raised.</li>
|
||||
* <li>In the case of a {@link Map} being used as a context, all possible accessors are
|
||||
* assumed to exist (but potentially return null), and no exception will ever be
|
||||
* raised.</li>
|
||||
* </ul>
|
||||
* Note that regardless of whether a null or default value is configured, if the resolution
|
||||
* of part of a compound key results in a missing or null value, an exception will be
|
||||
* raised. */
|
||||
public Compiler nullValue (String nullValue) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
nullValue, false, this.loader);
|
||||
}
|
||||
|
||||
/** Returns a compiler configured to use the supplied template loader to handle partials. */
|
||||
public Compiler withLoader (TemplateLoader loader) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode, this.defaultValue, loader);
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
this.nullValue, this.missingIsNull, loader);
|
||||
}
|
||||
|
||||
protected Compiler (boolean escapeHTML, boolean standardsMode, String defaultValue,
|
||||
TemplateLoader loader) {
|
||||
protected Compiler (boolean escapeHTML, boolean standardsMode,
|
||||
String nullValue, boolean missingIsNull, TemplateLoader loader) {
|
||||
this.escapeHTML = escapeHTML;
|
||||
this.standardsMode = standardsMode;
|
||||
this.defaultValue = defaultValue;
|
||||
this.nullValue = nullValue;
|
||||
this.missingIsNull = missingIsNull;
|
||||
this.loader = loader;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +133,7 @@ public class Mustache
|
||||
*/
|
||||
public static Compiler compiler ()
|
||||
{
|
||||
return new Compiler(true, false, null, FAILING_LOADER);
|
||||
return new Compiler(true, false, null, true, FAILING_LOADER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,10 +546,7 @@ public class Mustache
|
||||
super(name, segs, line);
|
||||
}
|
||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||
Object value = tmpl.getValue(ctx, _name, _line);
|
||||
if (value == null) {
|
||||
return; // TODO: configurable behavior on missing values
|
||||
}
|
||||
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
||||
if (value instanceof Iterable<?>) {
|
||||
value = ((Iterable<?>)value).iterator();
|
||||
}
|
||||
@@ -551,10 +581,8 @@ public class Mustache
|
||||
super(name, segs, line);
|
||||
}
|
||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||
Object value = tmpl.getValue(ctx, _name, _line);
|
||||
if (value == null) {
|
||||
executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values
|
||||
} else if (value instanceof Iterable<?>) {
|
||||
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
||||
if (value instanceof Iterable<?>) {
|
||||
Iterable<?> iable = (Iterable<?>)value;
|
||||
if (!iable.iterator().hasNext()) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -76,10 +77,12 @@ public class Template
|
||||
*
|
||||
* @param ctx the context in which to look up the variable.
|
||||
* @param name the name of the variable to be resolved, which must be an interned string.
|
||||
* @param missingIsNull whether to fail if a variable cannot be resolved, or to return null in
|
||||
* that case.
|
||||
*
|
||||
* @return the value associated with the supplied name or null if no value could be resolved.
|
||||
*/
|
||||
protected Object getValue (Context ctx, String name, int line)
|
||||
protected Object getValue (Context ctx, String name, int line, boolean missingIsNull)
|
||||
{
|
||||
if (!_compiler.standardsMode) {
|
||||
// if we're dealing with a compound key, resolve each component and use the result to
|
||||
@@ -89,10 +92,13 @@ public class Template
|
||||
// we want to allow the first component of a compound key to be located in a parent
|
||||
// context, but once we're selecting sub-components, they must only be resolved in the
|
||||
// object that represents that component
|
||||
Object data = getValue(ctx, comps[0].intern(), line);
|
||||
Object data = getValue(ctx, comps[0].intern(), line, missingIsNull);
|
||||
for (int ii = 1; ii < comps.length; ii++) {
|
||||
// generate more helpful error message
|
||||
if (data == null) {
|
||||
if (data == NO_FETCHER_FOUND) {
|
||||
throw new NullPointerException(
|
||||
"Missing context for compound variable '" + name + "' on line " + line +
|
||||
". '" + comps[ii - 1] + "' was not found.");
|
||||
} else if (data == null) {
|
||||
throw new NullPointerException(
|
||||
"Null context for compound variable '" + name + "' on line " + line +
|
||||
". '" + comps[ii - 1] + "' resolved to null.");
|
||||
@@ -101,7 +107,7 @@ public class Template
|
||||
// contexts; that would be weird and confusing
|
||||
data = getValueIn(data, comps[ii].intern(), line);
|
||||
}
|
||||
return data;
|
||||
return checkForMissing(name, line, missingIsNull, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,18 +122,40 @@ public class Template
|
||||
|
||||
// if we're in standards mode, we don't search our parent contexts
|
||||
if (_compiler.standardsMode) {
|
||||
return getValueIn(ctx.data, name, line);
|
||||
return checkForMissing(name, line, missingIsNull, getValueIn(ctx.data, name, line));
|
||||
}
|
||||
|
||||
boolean variableMissing = true;
|
||||
while (ctx != null) {
|
||||
Object value = getValueIn(ctx.data, name, line);
|
||||
if (value != null) {
|
||||
if (value == NO_FETCHER_FOUND) {
|
||||
// preserve variableMissing
|
||||
} else if (value == null) {
|
||||
// we found a fetcher, and it returned null; so we keep searching our parents, but
|
||||
// we won't freak out about a missing variable if we have a nullValue configured
|
||||
variableMissing = false;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
ctx = ctx.parent;
|
||||
}
|
||||
// we've popped off the top of our stack of contexts, so return null
|
||||
return null;
|
||||
// we've popped off the top of our stack of contexts, if we never actually found a fetcher
|
||||
// for our variable, we need to let checkForMissing() know
|
||||
return checkForMissing(name, line, missingIsNull, variableMissing ? NO_FETCHER_FOUND : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the specified variable, noting that it is intended to be used as the
|
||||
* contents for a segment. Presently this does not do anything special, but eventually this
|
||||
* will be the means by which we enact configured behavior for sections that reference null or
|
||||
* missing variables. Right now, all such variables result in a length 0 section.
|
||||
*/
|
||||
protected Object getSectionValue (Context ctx, String name, int line)
|
||||
{
|
||||
// TODO: configurable behavior on missing values
|
||||
Object value = getValue(ctx, name, line, _compiler.missingIsNull);
|
||||
// TODO: configurable behavior on null values
|
||||
return (value == null) ? Collections.emptyList() : value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,8 +164,11 @@ public class Template
|
||||
*/
|
||||
protected Object getValueOrDefault (Context ctx, String name, int line)
|
||||
{
|
||||
Object value = getValue(ctx, name, line);
|
||||
return (value == null) ? _compiler.defaultValue : value;
|
||||
Object value = getValue(ctx, name, line, _compiler.missingIsNull);
|
||||
// getValue will raise MustacheException if a variable cannot be resolved and missingIsNull
|
||||
// is not configured; so we're safe to assume that any null that makes it up to this point
|
||||
// can be converted to nullValue
|
||||
return (value == null) ? _compiler.nullValue : value;
|
||||
}
|
||||
|
||||
protected Object getValueIn (Object data, String name, int line)
|
||||
@@ -163,7 +194,7 @@ public class Template
|
||||
// if we were unable to create a fetcher, just return null and our caller can either try
|
||||
// the parent context, or do le freak out
|
||||
if (fetcher == null) {
|
||||
return null;
|
||||
return NO_FETCHER_FOUND;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -176,6 +207,20 @@ public class Template
|
||||
}
|
||||
}
|
||||
|
||||
protected Object checkForMissing (String name, int line, boolean missingIsNull, Object value)
|
||||
{
|
||||
if (value == NO_FETCHER_FOUND) {
|
||||
if (missingIsNull) {
|
||||
return null;
|
||||
} else {
|
||||
throw new MustacheException(
|
||||
"No method or field with name '" + name + "' on line " + line);
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
protected final Segment[] _segs;
|
||||
protected final Mustache.Compiler _compiler;
|
||||
protected final Map<Key, VariableFetcher> _fcache =
|
||||
@@ -339,6 +384,8 @@ public class Template
|
||||
}
|
||||
};
|
||||
|
||||
protected static final Object NO_FETCHER_FOUND = new Object();
|
||||
|
||||
protected static final String DOT_NAME = ".".intern();
|
||||
protected static final String THIS_NAME = "this".intern();
|
||||
protected static final String FIRST_NAME = "-first".intern();
|
||||
|
||||
@@ -113,6 +113,43 @@ public class MustacheTest
|
||||
"d", new Object[] { context("e", "3"), context("e", "4") })));
|
||||
}
|
||||
|
||||
@Test public void testNullSection () {
|
||||
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
Object foo = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testNullSectionWithDefaultValue () {
|
||||
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
Object foo = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testNullSectionWithNullValue () {
|
||||
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
Object foo = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testMissingSection () {
|
||||
test("", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
// no foo
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testMissingSectionWithDefaultValue () {
|
||||
test(Mustache.compiler().defaultValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
// no foo
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected=MustacheException.class)
|
||||
public void testMissingSectionWithNullValue () {
|
||||
test(Mustache.compiler().nullValue(""), "", "{{#foo}}{{bar}}{{/foo}}", new Object() {
|
||||
// no foo
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testComment () {
|
||||
test("foobar", "foo{{! nothing to see here}}bar", new Object());
|
||||
}
|
||||
@@ -215,6 +252,28 @@ public class MustacheTest
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected=NullPointerException.class)
|
||||
public void testNullComponentInCompoundVariable () {
|
||||
// make sure that even if we have a null value configured, we freak out
|
||||
test(Mustache.compiler().nullValue("foo"), "unused", "{{foo.bar.baz}}", new Object() {
|
||||
Object foo () {
|
||||
return new Object() {
|
||||
Object bar = null;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected=NullPointerException.class)
|
||||
public void testMissingComponentInCompoundVariable () {
|
||||
// make sure that even if we have a default value configured, we freak out
|
||||
test(Mustache.compiler().defaultValue("foo"), "unused", "{{foo.bar.baz}}", new Object() {
|
||||
Object foo () {
|
||||
return new Object(); // no bar
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testNewlineSkipping () {
|
||||
String tmpl = "list:\n" +
|
||||
"{{#items}}\n" +
|
||||
@@ -322,6 +381,23 @@ public class MustacheTest
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected=MustacheException.class)
|
||||
public void testMissingValueWithNullDefault () {
|
||||
test(Mustache.compiler().nullValue(""),
|
||||
"bar", "{{missing}}{{notmissing}}", new Object() {
|
||||
String notmissing = "bar";
|
||||
// no field or method for 'missing'
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testNullValueGetsNullDefault () {
|
||||
test(Mustache.compiler().nullValue("foo"),
|
||||
"foobar", "{{nullvar}}{{nonnullvar}}", new Object() {
|
||||
String nonnullvar = "bar";
|
||||
String nullvar = null;
|
||||
});
|
||||
}
|
||||
|
||||
protected void test (Mustache.Compiler compiler, String expected, String template, Object ctx)
|
||||
{
|
||||
assertEquals(expected, compiler.compile(template).execute(ctx));
|
||||
|
||||
Reference in New Issue
Block a user