; last updated - 5 minutes read

Update February 2019, 26

This article describes one of my first attempts to make JSF a developer-friendly space. It's what I did seven years ago. In the meantime, I've first implemented the feature in AngularFaces. Later, I've migrated it to BootsFaces. Since version 1.1.0 of BootsFaces, our JSF components recognize the validation constraints of the beans without further ado. You don't have to do anything, it just works. Exactly as it should be.

That's how my journey into JSF in particular and open-source software development in general began.

JSF and the DRY[1] principle

Where do you put the plausibility constraints? Beginning with JSF 2.0, JSF offers two possibilities. You can use the traditional way by declaring the constraints in the JSF file:

Or you can use the bean validation API:

@NotNull @Size(max=30) String firstName;

I prefer the bean validation annotations: most constraints belong to business logic. In most cases, this means the beans are a good place to put the constraints. On the other hand, omitting the JSF tags results in bad user experience because the input won't be verified immediately when typing. Not a good choice. My solution is to copy the beans' constraints to the JSF files, and I've written a small program to do the merging. If you use build scripts, it's a natural choice to integrate my program in the build process.

Feel free to use the source code if you need it. Please note that I'm providing it on an "as-is" basis: if you use the source code, you use it at your own risk. It's a sketchy implementation, tested in simple cases only. My tests don't cover every class of the real word, and anyway, there is much room for improvements. If you've got a clever idea on how to improve my code, I'd like to hear from you! You can download my prototype here.

What it does

My example implementation currently extracts two annotations:

  • @NotNull ist translated into required="true"
  • @Size(max="30") is translated into maxlength="30"
  • @Size(min="5", max="30") is translated into required="true" maxlength="30"

Before you ask: yes, the latter example is a bit fishy. There's no minlength attribute in HTML, so the best approach is to make the field a required field.

Update February 26, 2019: At least I thought so when writing this article. I didn't take into account that fields with a minimum size don't are not necessarily mandatory.

Limitations

There are a lot of useful annotations my sample implementation ignores: @Min, @Max, @Pattern, just to name a few.

Currently, my implementation analyzes input fields only. But it's not a big deal to include other tags like inputTextarea or inputMask. It's your turn!

Requirements

The source code requires the GSon library . However, you can easily do without: only a few number of lines rely on this library.

For the sake of completeness, I should mention you also need the JSF and JSR 303 libraries to compile the code.

The API

It's simple. Just call the method Merger.mergeBeanValidationIntoJSF(), providing the folder the beans are stored in, the folder containing the JSF files and the folder to store the modified JSF file.

class MergerTest extends GroovyTestCase { void testMerger() { new Merger().mergeBeanValidationIntoJSF ("src/de/beyondJava/beans", "src/de/beyondJava/beans", "generated") } }

You can integrate this into an ant script using the groovy task of ant.

The implementation

The merging algorithm is located in the class Merger. It consists of merely 88 lines, so I guess a short description will do. I'm sure you'll find it's easy to add features.

The heart of the implementation is the SimpleClassParser I mentioned a couple of days ago (see previous blog entry). It is used by the SimpleBeanParser to get the complete list of variables that can be used in a JSF file.

The JSF file can be read by Groovy's XMLParser:

def jsf = new XmlParser().parse(p_sourceFile); def inputs = jsf['**'] def inputFields = inputs.findAll{ it.name().toString().endsWith("inputText")} inputFields.each{ addTags(it, vars)}

If the node bears a value that consists of a bean variable, it looks up this variable's annotations. If it finds a @NotNull annotation, it add the required="true" tag by using Groovy's metaprogramming (p_node.@required="true").

The @Size tag is treated similarly. However, the implementation is slightly more verbose because the annotation bears one or two parameters.

private void addTags(def p_node, List vars) { String value = p_node.@value if (value[2]=="#{" && value[-1]=="}") { String potentialVariable = value[3] VariableDefinition match = vars.find{VariableDefinition v -> v.name == potentialVariable} if (match) { if (match.annotations.any{it=="@NotNull"}) { p_node.@required="true" } String sizeTag = match.annotations.find{it=~ /@Size(\s)*\((.)*\)/} if (sizeTag) { sizeTag=sizeTag.replaceAll(/@Size(\s)*\(/, "").trim() String[] sizes = sizeTag.[4]split(",") sizes.each{String s -> if (s.trim().startsWith("max")) addMaxLengthTag(p_node, s) } sizes.each{String s -> if (s.trim().startsWith("min")) p_node.@required="true" } } } } } private void addMaxLengthTag(def p_node, String p_maxLength) { String[] terms = p_maxLength.split("=") String maxLength = terms[1].trim() int iMaxLength = new Integer(maxLength).intValue() if (p_node.@maxlength) { if ("$iMaxLength" != p_node.@maxlength) { println "Warning: Bean Validation Constraint differs from JSF maxlength attribute in input field ${p_node.@value}: ${p_node.@maxlength} (JSF) vs. $iMaxLength (bean annotation)" } } else p_node.@maxlength="$iMaxLength" }
  1. "don't repeat yourself" - a common wisdom telling you to avoid code duplication↩
  2. .1↩
  3. .-2↩
  4. .-2↩

Comments