Problem with thymeleaf expression utility #lists.contains()

classic Classic list List threaded Threaded
9 messages Options
Reply | Threaded
Open this post in threaded view
|

Problem with thymeleaf expression utility #lists.contains()

tlerma909
I'm working with the thymeleaf standard dialect and trying to render a list of checkboxes in a form.  The rendering is ok, however, the problem is where I try to apply the "checked" property to the checkboxes using the thymeleaf #lists.contains() expression utility method.  I also posted this question to stack overflow but have not received any responses at this point and this is the last thing holding me up on this particular form I'm working on.

So I have a model class that has the following fields:

private List<Template> templates;
       
@FormParam("selectedTemplates")
private List<String> selectedTemplates = Lists.newArrayList();


A Thymeleaf template html fragment:

<div th:each="template : *{templates}">
        <input type="checkbox" name="selectedTemplates" th:value="${template.id}"
                th:checked="${#lists.contains(product.selectedTemplates, template.id)}" />
        <label th:text="${template.filename} + ' (' + ${template.description} + ')'" />
       
        <div th:text="${product.selectedTemplates}"/>
        <div th:text="${template.id}"/> 
        <div th:text="${#lists.contains(product.selectedTemplates, template.id)}" />
</div>

The output on the page for one of the text boxes that should be selected.

    <div>                               
    <input type="checkbox" name="selectedTemplates" value="4">
    <label>Document Template (Description!)</label>
    <div>[4, 5]</div>
    <div>4</div> 
    <div>false</div>
    </div>

So as you can see, I print the list which has values [4,5] and I use the #lists.contains method to see if it has template.id in it, however, the method always returns false. I even tried some hard coded ids to test the method and I always get "false" back.

For example:

<div th:text="${product.selectedTemplates}"/>
<div th:text="${#lists.contains(product.selectedTemplates, 4)}" />

Prints <div>[4,5]</div><div>false</div>

<div th:text="${product.selectedTemplates}"/>
<div th:text="${#lists.contains(product.selectedTemplates, '4')}" />

Prints <div>[4,5]</div><div>false</div>

Not sure what I'm doing wrong, but it seems so straight forward, not sure what else to try.  Any suggestions or advice is greatly appreciated.
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

tlerma909
Is this a bug?  Still fighting with this issue, and I added a test case which revealed what I can only fathom is a bug.  

@FormParam("testList")
private List<String> testList = Lists.newArrayList();

Then I added some test values to the list:

testList.add("test1");
testList.add("test2");
testList.add("3");
testList.add("P");

Then in thymeleaf, I wrote the following statements:

<div th:text="${product.testList}"/>
<div th:text="${#lists.contains(product.testList, 'test1')}"/>
<div th:text="${#lists.contains(product.testList, 'test2')}"/>
<div th:text="${#lists.contains(product.testList, '3')}"/>
<div th:text="${#lists.contains(product.testList, 'P')}"/>

Which yields the following output:

<div>[test1, test2, 3, P]</div>
<div>true</div>
<div>true</div>
<div>false</div>
<div>false</div>

So as you can see when using #lists.contains() for a string of length 1, I always get "false" back.  I'm working with thymeleaf-2.0.19.jar in my project, and noticing that there is a 2.1.1.RELEASE available, I tried that with the same results.  Is there any other way of getting my checkboxes to check in thymeleaf?  Unfortunately, I'm not using spring so can't use the "th:field" tag.  


Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

tlerma909
Luckily this is a new project and the solution I came up with was to simply reset the sequences driving the id's so that I don't have any single digit ids to work with.  As long as the value passed into the #lists.contains() has at least two characters, then it works fine.  Also, I discovered that I needed to add an empty character to the id value I was using to check the list:

th:checked="${#lists.contains(product.selectedTemplates, '' + template.id)}"

Without the '' + portion, I would always get a "false" result.

Not sure what I would have done if I couldn't simply update the sequences in an existing project, I'd say this is a bug as I don't see any reason this shouldn't work on a single character string.  
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

Antibrumm
I just tested this on a spring-thymeleaf project. On my side this works fine

spring-3.2.3.RELEASE
thymeleaf-2.1.2.RELEASE

controller
@Controller
public class HomeController {
	@ModelAttribute("testList")
	public List<String> getTestList() {
		List<String> testList = new ArrayList<>();
		testList.add("test1");
		testList.add("test2");
		testList.add("3");
		testList.add("P");
		return testList;
	}

	@RequestMapping(value = "/home")
	public String getHome() {
		return "home";
	}
}

home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>
<body>
	<div th:text="${testList}"></div>
	<div th:text="${#lists.contains(testList, 'test1')}"></div>
	<div th:text="${#lists.contains(testList, 'test2')}"></div>
	<div th:text="${#lists.contains(testList, '3')}"></div>
	<div th:text="${#lists.contains(testList, 'P')}"></div>
	<div th:text="${#lists.contains(testList, 'not')}"></div>
</body>
</html>

Output
[test1, test2, 3, P]
true
true
true
true
false
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

Emanuel
Administrator
In reply to this post by tlerma909
I created a test case of the example from your second post and was able to debug what was going on.  The problem seemed to be that the single-character comparisons were failing because the passed object was Character, not String.  The expression language parser, OGNL, has these rules about Strings and Characters:

OGNL has the following kinds of constants:

 * String literals, as in Java (with the addition of single quotes): delimited by single- or double-quotes, with the full set of character escapes;
 * Character literals, also as in Java: delimited by single-quotes, also with the full set of escapes;
(from: http://commons.apache.org/proper/commons-ognl/language-guide.html)


Because we can't use double-quotes to wrap those single-character strings, they end up as Character types instead of String types, and then fail the equality test in the contains() method.

Seeing as it's HTML, I was able to put a double-quote in there by using the HTML entity code for it: &quot;
So the last 2 lines became:

<div th:text="${#lists.contains(testList, &quot;3&quot;)}"/>
<div th:text="${#lists.contains(testList, &quot;P&quot;)}"/>

Those then passed the contains() test.

---
Re: Antibrumm's post - in a Spring project, SpringEL would be used instead, which has slightly different handling of expressions than OGNL, so might explain why the test passes in a Spring environment.
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

Antibrumm
Thanks Emanuel for the explanaition.

One thing is still strange though:

It's understandable why this happens if the test uses hardcoded values which might get evaluated a character.

In the initial example of tlerma909's post, where you use another variable reference in the contains like  "template.id" this OGNL specificity should not be triggered i would say.
Does this mean that OGNL first evaluation of "template.id" is also resulting in a char?


I think the real question is why is this happening on the call "${#lists.contains(List<String> listReference, String strReference)}"
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

tlerma909
Thank you both for the feedback.  This basically brings me full circle into realizing that really all I was missing in my initial example was the adding of an empty string to template.id

'' + template.id

When working with thymeleaf, I have a tendency to forget about the underlying types that drive all of this in the back end.  In suspecting an issue with the method itself, I had no idea how the hard-coded values were being treated in the thymeleaf code (thanks for clearing that up Emanuel), thus leading me to the wrong conclusion.  So basically, what I was initially trying to do translated into java as:

selectedTemplates.contains(template.getId());

Which will always return false given that the id is a "Long" type, and then adding the empty string casts it to string and the method call then works as expected:

selectedTemplates.contains("" + template.getId());

Not certain why it works without issue for Antibrumm, do you suppose the hard coded values are treated differently in the newest version of thymeleaf and not as characters?  

Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

Antibrumm
So why, OGNL and SpringEL differences aside, are you not just simply using a "List<Long> selectedTemplates"?

No cast necessary and you would not have had the String.equals(Long) problem in the first place ;)
Reply | Threaded
Open this post in threaded view
|

Re: Problem with thymeleaf expression utility #lists.contains()

tlerma909
What you say is very true and is the way I should have implemented it.  I decided to change the longs to strings in this form because I have several optional foreign keys that when they are of type Long and not selected in the form, I get an error:

HTTP Status 400 - Unable to extract parameter from http request: javax.ws.rs.FormParam("styleId") value is '' for private java.lang.Long com.test.template.TemplateForm.styleId

I know that there are other ways around this, like specifying a default value in the form for optional fields and checking that value before trying to persist, but I chose to simply convert the Long to a String thinking it would be easier overall. I think I just got set in "String" mode and was doing that for all fields, when it makes good sense to keep the Long type for the checkboxes discussed in this thread.  Thanks for the advice.