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:
Michael Bayne
2011-05-28 10:42:45 -07:00
parent ef971015bd
commit 9fae13e008
4 changed files with 214 additions and 35 deletions
+31 -3
View File
@@ -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));