Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Null handling when iterating String arrays/lists #202

Open
softwaremaverick opened this issue Jan 24, 2018 · 12 comments
Open

Null handling when iterating String arrays/lists #202

softwaremaverick opened this issue Jan 24, 2018 · 12 comments

Comments

@softwaremaverick
Copy link

Given this example:

import java.io.StringReader;
import java.io.StringWriter;

import org.testng.annotations.Test;

import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;


public class SimpleMustacheTest
{
private static class MyObject
{
	public String[] getNames()
	{
		return new String[] { "Fred", null };
	}
}


@Test
public void simpleTest()
{
	final String template = "{{#names}}{{.}}\n\n{{/names}}";

	final MustacheFactory mustacheFactory = new DefaultMustacheFactory();
	final Mustache mustache = mustacheFactory.compile(new StringReader(template), "test");

	final StringWriter writer = new StringWriter();
	mustache.execute(writer, new MyObject());

	System.out.println(writer.toString());
}
}

Why do I get this?

Fred
SimpleMustacheTest$MyObject@4891a775

Effectively it looks like it's saying that if a value cannot be extracted for an element then I'll take the parent scope. If I'm doing a {{.}} then that parent scope seeking shouldn't happen should it? It's not like anyone would be saying that if I don't have a value then get my parent instead would they?

If this is a design decision then does that mean we have to wrap all objects in true/false checks?

Currently the scenario in the real code I'm forcing the data to always return empty string if the value is a null to overcome this issue.

@spullara
Copy link
Owner

It is an odd case that you have nulls in your lists, this hasn't come up before, but I agree that {{.}} probably shouldn't go up the stack to the next object. This wasn't an explicit design decision.

@softwaremaverick
Copy link
Author

softwaremaverick commented Jan 24, 2018 via email

@softwaremaverick
Copy link
Author

softwaremaverick commented Jan 24, 2018 via email

@spullara
Copy link
Owner

This issue may be more fundamental than I thought. Basically as it is creating the list of scopes it ignores scopes that are null. The . references the latest scope and since the value was null the iterator itself is the latest scope in this case. I'm investigating how much that assumption is strung through the code.

@spullara
Copy link
Owner

People are definitely using null as an indicator to look to parent scopes since that is the normal behavior. I might be able to special case for just '.' though because it is already a very special case.

@spullara
Copy link
Owner

I added a test that shows you how to construct a MustacheFactory that has the behavior that you need. This kind of change is one of the reasons that almost everything is overridable and I don't need to change it for everyone or add a configuration parameter for users to get whatever custom behavior they need. I don't think that you will run into any other problems using it like this since all of my tests pass locally when I tried to make the global change, just worried there may be some edge case out there that would break.

c762c26

Can you try this in your program and see if it gives you what you need?

@softwaremaverick
Copy link
Author

softwaremaverick commented Jan 25, 2018

Thanks for that @spullara. However, the code you provided above I suspect is for 0.9.x as I got an Override warning as the method wasn't being overridden.

Modifying my 0.8.12 (as that is what we currently use), I made the following using an empty string instead of a null.

final MustacheFactory mustacheFactory = new DefaultMustacheFactory()
{
	@Override
	public MustacheVisitor createMustacheVisitor()
	{
		return new DefaultMustacheVisitor(this)
		{
			@Override
			public void iterable(final TemplateContext templateContext, final String variable,
					final Mustache mustache)
			{
				this.list.add(new IterableCode(templateContext, this.df, mustache, variable)
				{
					@Override
					protected Object[] addScope(final Object[] scopes, final Object scope)
					{
						return super.addScope(scopes, scope == null ? StringUtils.EMPTY : scope);
					}
				});
			}
		};
}
};

I'm currently running a full build of tests to see if anything comes out of it, but the few tests I ran worked fine with it. I don't see any potential issues arising from using an empty string either.

I've tested this with both the {{.}} and specifying {{value}} to test whether reflection trying to find the value from a String would return a value (after modifying the string to return the word Test instead of empty string of course), but it didn't which was expected and so an empty string in that test was returned.

What do you think?

@spullara
Copy link
Owner

LGTM. Please close the issue when you are convinced it is working for you.

@softwaremaverick
Copy link
Author

softwaremaverick commented Jan 26, 2018 via email

@spullara
Copy link
Owner

I need to convince myself that it doesn't break backwards compatibility with some wanted behavior.

@xjdsg
Copy link

xjdsg commented Mar 5, 2020

@spullara I am using mustache 0.9.6 and I found one issue with overriding the addScope as below:
protected boolean addScope(List<Object> scopes, Object scope) { scopes.add(scope); return true; }
The issue happened when iterating List, and the first object of the List is null,
With the test case
{ "content": { "values": [ null, { "name": "Alice" }, { "name": "Bob" } ] } }
And
{{#content}} {{#values}} {{name}} {{/values}} {{/content}}
The result after compiling the mustache is empty string \n \n \n instead of \n Alice\n Bob\n
The root cause is when resolving the null value,
the scopes is [content, values, null]
it will create a wrapper with
LengthGuard(length 3) -> MapGuard (index 1) -> MapGuard(index 0)... finally wrap as MissingWrapper

When iterating the second value Alice, the scopes is
[content, values, name]
it will try to find the right wrapper in prevWrapper, which is Missing Wrapper now, and it will find all the things are matching, and finally get MissingWrapper, and can't find the right value Alice. The problem is it has missed the index 2 in the scopes.
using
protected Object[] addScope(final Object[] scopes, final Object scope) { return super.addScope(scopes, scope == null ? StringUtils.EMPTY : scope); }

would not have this problem, because it has replace the null value with string class. I could use it as a temporary solution.

But could you include the fix or considering this problem into the official build?

There are several solutions I could think of:

  1. ReflectionObjectHandler.find() function, line 67, if the scope is null, still create a ClassGuard for it and then continue
  2. As prevWrapper is used for improving the performance, I am wondering if it's valuable to put the MissingWrapper in it. Maybe we can move the MissingWrapper out of it.

@spullara
Copy link
Owner

spullara commented Mar 6, 2020

Thanks for the report. My guess is that the first solution might work. Taking a loot at it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants