jte icon indicating copy to clipboard operation
jte copied to clipboard

Unable to handle model properties that contain dots (`.`)

Open checketts opened this issue 1 year ago • 7 comments

With Spring validation, when a form entry has an error Spring adds a BindingResult to the model with the key similar to org.springframework.validation.BindingResult.myEntry. However there isn't a way to reference the model entry via a @param

@param org.springframework.validation.BindingResult.myEntry: org.springframework.validation.BeanPropertyBindingResult

Since there are dots in the variable name that isn't allowed.

Would a PR for the Spring integration that changes the dots for underscores be welcome?

I'm looking to see if there are other options as well...

checketts avatar May 20 '24 21:05 checketts

Hi @checketts,

thanks for reporting and, ooof, this is wild. I'm not a Spring user, but modifying Spring keys to match jte parameter name syntax is asking for trouble. For instance, once Spring starts having keys with underscores that might collide with the hacked keys.

I'm not sure, but maybe there could be other ways to access those things into the jte templates?

Input from real Spring - jte users would be very welcome here :-)

casid avatar May 21 '24 16:05 casid

The way I've worked around it in my code is to create a ValidationHelper that looks up the validation results. I'll iterate on it a few times, then we could look into adding that in the Spring library and make it configurable.

For JTE, when populating a @param would it be worth supporting handling the periods in model keys? Like mapping those to params with underscores?

For example (in a kte file):

@param org_springframework_validation_BindingResult_myEntry: org.springframework.validation.BeanPropertyBindingResult

Then if there is a key with underscores that would match, it would be used first, and if not, then check for a key with periods?

checketts avatar May 21 '24 17:05 checketts

No, I would rather not introduce this kind of magic to jte.

It will add a performance overhead for every template invocation through maps. It also obfuscates constants (e.g. in your example a project wide search will not find org.springframework.validation.BindingResult.myEntry in jte templates).

Adding a utility method to the spring lib sounds like a much better API for this problem.

casid avatar May 23 '24 03:05 casid

Hi @checketts, did you get around to iterating on the ValidationHelper and if yes, are your results available somewhere?

frederikb avatar Nov 06 '24 22:11 frederikb

I'll try to release a sample project shortly. My current solution has worked well, but feels quite hacky. (Since it was influenced by the Thymeleaf pattern to aid in migration) I use a custom JteView, set the model into my Base class when possible and have a helper on that class (see base?.let { it.renderModel = model }):


class MyJteView(
    private val templateEngine: TemplateEngine,
    private val messageSource: MessageSource,
    private val isDevelopmentMode: Boolean
) :
    AbstractTemplateView() {
    override fun checkResource(locale: Locale): Boolean {
        return try {
            if (isDevelopmentMode) {
                true //In development mode new templates may get added dynamically
            } else {
                templateEngine.hasTemplate(this.url)
            }
        } catch (e: Exception) {
            true
        }
    }

    @Throws(Exception::class)
    override fun renderMergedTemplateModel(
        model: Map<String, Any>,
        request: HttpServletRequest,
        response: HttpServletResponse
    ) {
        val url = this.url
        response.contentType = "text/html"
        response.characterEncoding = StandardCharsets.UTF_8.name()
        val output = PrintWriterOutput(response.writer)


        try {
            val base = model["base"] as? Base
            base?.let { it.renderModel = model }


            templateEngine.render(url, model, output)
        } catch (e: Exception) {
//            response.sendError(500);
            logger.error("Error rendering template: ", e)
            response.writer.println("<html><body><pre>Error rendering template:\n")
            val out = PrintWriterOutput(response.writer)
            Escape.htmlContent(e.localizedMessage, out)
            if (e.cause != null) {
                Escape.htmlContent(e.cause!!.localizedMessage, out)
            }
            response.writer.println("</pre></body></html>")
        } finally {
//            JteContext.dispose()
        }
    }
}

Base:

data class Base(
    val user: UserPrincipal?,
) {
    var renderModel: Map<String, Any>? = null

    fun errors(key: String): BeanPropertyBindingResult? {
        val errors = renderModel?.get("org.springframework.validation.BindingResult.$key") as? BeanPropertyBindingResult

        return errors
    }
}

checketts avatar Nov 07 '24 02:11 checketts

I too am keen to see @checketts 's ValidationHelper :)

Having access to binding results is the only real thing stopping me converting my project from Thymeleaf to JTE, and maybe Spring View Components too.

linus1412 avatar Nov 29 '24 21:11 linus1412

@linus1412 I am guessing you found a solution.

This is what I use in case someone needs it.

@NoArgsConstructor
@AllArgsConstructor
public class JteValidationHelper {
    private BindingResult bindingResult;

    public boolean hasErrors() {
        return bindingResult != null && bindingResult.hasErrors();
    }

    public boolean hasError(String name) {
        return bindingResult != null && bindingResult.hasFieldErrors(name);
    }

    public List<FieldError> getErrors() {
        if(bindingResult == null) {
            return Collections.emptyList();
        } else {
            return bindingResult.getFieldErrors();
        }
    }

    public FieldError getError(String fieldName) {
        if(bindingResult == null) {
            return null;
        } else {
            return bindingResult.getFieldError(fieldName);
        }
    }
}

Add it to the model:

model.addAttribute("validation", new JteValidationHelper(bindingResult));

Use it it the template:

@if(validation.hasErrors())
     NGGGGGH! Stupid ${validation.getErrors().get(0).getField()} field. 
     ${validation.getErrors().get(0).getDefaultMessage()}
@endif

michaelkrog avatar Jan 30 '25 16:01 michaelkrog