Validating JSF EL Expressions in JSF Pages with static-jsfexpression-validator
static-jsfexpression-validator is utility for verifying that EL expressions in JSF pages, such as
#{bean.property}
, are correct, that means that they don't reference undefined managed beans and nonexistent getters or action methods. The purpose is to make JSF-based web applications safer to refactor as the change of a method name will lead to the detection of an invalid expression without need for extensive manual UI tests. It can be run statically, for example from a test. Currently it builds on the JSF implementation v. 1.1 but can be in few hours (or days) modified to support newer version of JSF. How does it work?- Defined managed beans (name + type) are extracted from faces-config files and/or Spring application context files
- JSP pages are parsed by Jasper, Tomcat's JSP parser
- For each JSF tag:
- If it defines local variables, they are recorded (such as var in h:dataTable)
- All JSF EL expressions in the tag's attributes are validated by a real EL resolver using two magic classes, namely custom VariableResolver and PropertyResolver, that - instead of looking up managed bean instances and invoking their getters - fabricate "fake values" of the expected types so that the "resolution" of an expression can proceed. The effect is that the existence of the referenced properties and action methods is verified against the target classes.
- Sometimes it is not possible to determine the type of a JSF variable or property (e.g. when it's a Collection element), in which case it is necessary to declare it beforehand.
- You can also manually declare extra variables (managed beans) and override the detected type of properties.
Minimal Setup
Add this dependency to your Maven/Ivy/... (update: Ant is not really needed if you are executing the validator only from ant):
net.jakubholy.jeeutils.jsfelcheck
static-jsfexpression-validator-jsf11
<!-- <artifactId>static-jsfexpression-validator-jsf12</artifactId> -->
<!-- <artifactId>static-jsfexpression-validator-jsf20</artifactId> - now only reuses 1.2 -->
0.9.3
test
Alternatively, you can fetch static-jsfexpression-validator-jsf11-0.9.3.jar (or -jsf12- or -jsf20-) and its dependencies yourself, see the Appendix A.
Run it:
java -cp static-jsfexpression-validator-jsf11-0.9.3.jar:... net.jakubholy.jeeutils.jsfelcheck.JsfStaticAnalyzer --jspRoot /path/to/jsp/files/dir
Alternatively, run it from a Java class to be able to configure everything:
public class JsfElValidityTest {
@Test
public void should_have_only_defined_beans_and_valid_properties_in_jsf_el_expressions() throws Exception {
JsfStaticAnalyzer jsfStaticAnalyzer = new JsfStaticAnalyzer();
jsfStaticAnalyzer.setFacesConfigFiles(Collections.singleton(new File("web/WEB-INF/faces-config.xml")));
Map> none = Collections.emptyMap();
CollectedValidationResults results = jsfStaticAnalyzer.validateElExpressions("web", none, none, none);
assertEquals("There shall be no invalid JSF EL expressions; check System.err/.out for details. FAILURE " + results.failures()
, 0, results.failures().size());
}
}
Run it and check the standard error and output for results, which should ideally look something like this:
INFO: >>> STARTED FOR '/someFile.jsp #############################################
...
>>> LOCAL VARIABLES THAT YOU MUST DECLARE TYPE FOR [0] #########################################
>>> FAILED JSF EL EXPRESSIONS [0] #########################################
(Set logging to fine for class net.jakubholy.jeeutils.jsfelcheck.validator.ValidatingJsfElResolver to se failure details and stacktraces)
>>> TOTAL EXCLUDED EXPRESIONS: 0 by filters: []
>>> TOTAL EXPRESSIONS CHECKED: 5872 (FAILED: 0, IGNORED EXPRESSIONS: 0) IN 0min 25s
Standard Usage
Normally you will need to configure the validator because you will have cases where property type etc. cannot be derived automatically.Declaring Local Variable Types, Extra Variables, Property Type Overrides
Local Variables - h:dataTable etc.
If your JSP includes a JSF tag that declares a new local variable (typically h:dataTable), like vegetable in the example below:
...
where favouriteVegetable is a
Collection
of Vegetable
s then you must tell the validator what type of objects the collection contains:
Map> localVariableTypes = new Hashtable>();
localVariableTypes.put("vegetarion.favouriteVegetable", Vegetable.class);
jsfStaticAnalyzer.validateElExpressions("web", localVariableTypes, extraVariables, propertyTypeOverrides);
The failure to do so would be indicated by a number of failed expression validations and a suggestion to register type for this variable:
>>> LOCAL VARIABLES THAT YOU MUST DECLARE TYPE FOR [6] #########################################
Declare component type of 'vegetarion.favouriteVegetable' assigned to the variable vegetable (file /favourites.jsp, tag line 109)
>>> FAILED JSF EL EXPRESSIONS [38] #########################################
(Set logging to fine for class net.jakubholy.jeeutils.jsfelcheck.validator.ValidatingJsfElResolver to se failure details and stacktraces)
FailedValidationResult [failure=InvalidExpressionException [Invalid EL expression '#{vegetable.name}': PropertyNotFoundException - Property 'name' not found on class net.jakubholy.jeeutils.jsfelcheck.expressionfinder.impl.jasper.variables.ContextVariableRegistry$Error_YouMustDelcareTypeForThisVariable$$EnhancerByMockitoWithCGLIB$$3c8d0e8f]; expression=#{vegetable.name}, file=/favourites.jsp, tagLine=118]
Defining Variables Not in faces-config
Variable: the first element of an EL expression.If you happen to be using a variable that is not a managed bean defined in faces-config (or Spring config file), for example because you create it manually, you need to declare it and its type:
Map> extraVariables = new Hashtable>();
localVariableTypes.put("myMessages", Map.class);
jsfStaticAnalyzer.validateElExpressions("web", localVariableTypes, extraVariables, propertyTypeOverrides);
Expressions like
#{myMessages['whatever.key']}
would be now OK.
Overriding the Detected Type of Properties, Especially for Collection Elements
Property: any but the first segment of an EL expression (#{variable.propert1.property2['property3]....}).Sometimes you need to explicitely tell the validator the type of a property. This is necessary if the poperty is an object taken from a Collection, where the type is unknown at the runtime, but it may be useful also at other times.
If you had:
then you'd need to declare the type like this:
Map> propertyTypeOverrides = new Hashtable>();
propertyTypeOverrides.put("vegetableMap.*", Vegetable.class);
//or just for 1 key: propertyTypeOverrides.put("vegetableMap.carrot", Vegetable.class);
jsfStaticAnalyzer.validateElExpressions("web", localVariableTypes, extraVariables, propertyTypeOverrides);
Using the .* syntax you indicate that all elements contained in the Collection/Map are of the given type. You can also override the type of a single property, whether it is contained in a collection or not, as shown on the third line.
Excluding/Including Selected Expressions for Validation
You may supply the validator with filters that determine which expressions should be checked or ignored. This may be useful mainly if you it is not possible to check them, for example because a variable iterates over a collection with incompatible objects.The ignored expressions are added to a separate report and the number of ignored expressions together with the filters responsible for them is printed.
Example: ignore all expressions for the variable
evilCollection
:
jsfStaticAnalyzer.addElExpressionFilter(new ElExpressionFilter(){
@Override public boolean accept(ParsedElExpression expression) {
if (expression.size() == 1
&& expression.iterator().next().equals("evilCollection")) {
return false;
}
return true;
}
@Override public String toString() {
return "ExcludeEvilCollectionWithIncompatibleObjects";
}
});
(I admit that the interface should be simplified.)
Other Configuration
In JsfStaticAnalyzer:- setFacesConfigFiles(Collection<File>): faces-config files where to look for defined managed beans; null/empty not to read any
- setSpringConfigFiles(Collection<File>) Spring applicationContext files where to look for defined managed beans; null/empty not to read any
- setSuppressOutput(boolean) - do not print to System.err/.out - used if you want to process the produced CollectedValidationResults on your own
- setJspsToIncludeCommaSeparated(String) - normally all JSPs under the jspDir are processed, you can force processing only the ones you want by supplying thier names here (JspC setting)
- setPrintCorrectExpressions(boolean) - set to true to print all the correctly validated JSF EL expressions
Understanding the Results
jsfStaticAnalyzer.validateElExpressions prints the results into the standard output and error and also returnes them in a CollectedValidationResults with the following content:- ResultsIterable<FailedValidationResult> failures() - expressions whose validation wasn't successful
- ResultsIterable<SuccessfulValidationResult> goodResults() - expressions validated successfully
- ResultsIterable<ExpressionRejectedByFilterResult> excluded() - expressions ignored due to a filter
- Collection<DeclareTypeOfVariableException> - local variables (h:dataTable's var) for which you need to declare their type
Now we will look how the results appear in the output.
Unknown managed bean (variable)
FailedValidationResult [failure=InvalidExpressionException [Invalid EL expression '#{messages['message.res.ok']}': VariableNotFoundException - No variable 'messages' among the predefined ones.]; expression=#{messages['message.res.ok']}, file=/sample_failures.jsp, tagLine=20]
Solution: Fix it or add the variable to the
extraVariables
map parameter.
Invalid property (no corresponding getter found on the variable/previous property)
a) Invalid property on a correct target object class
This kind of failures is the raison d'être of this tool.
FailedValidationResult [failure=InvalidExpressionException [Invalid EL expression '#{segment.departureDateXXX}': PropertyNotFoundException - Property 'departureDateXXX' not found on class example.Segment$$EnhancerByMockitoWithCGLIB$$5eeba04]; expression=#{segment.departureDateXXX}, file=/sample_failures.jsp, tagLine=92]
Solution: Fix it, i.e. correct the expression to reference an existing property of the class. If the validator is using different class then it should then you might need to define a propertyTypeOverride.
b) Invalid property on an unknown target object class - MockObjectOfUnknownType
FailedValidationResult [failure=InvalidExpressionException [Invalid EL expression '#{carList[1].price}': PropertyNotFoundException - Property 'price' not found on class net.jakubholy.jeeutils.jsfelcheck.validator.MockObjectOfUnknownType$$EnhancerByMockitoWithCGLIB$$9fa876d1]; expression=#{carList[1].price}, file=/cars.jsp, tagLine=46]
Solution: carList is clearly a List whose element type cannot be determined and you must therefore declare it via the
propertyTypeOverrides
map property.
Local variable without defined type
FailedValidationResult [failure=InvalidExpressionException [Invalid EL expression ' #{traveler.name}': PropertyNotFoundException - Property 'name' not found on class net.jakubholy.jeeutils.jsfelcheck.expressionfinder.impl.jasper.variables.ContextVariableRegistry$Error_YouMustDelcareTypeForThisVariable$$EnhancerByMockitoWithCGLIB$$b8a846b2]; expression= #{traveler.name}, file=/worldtravels.jsp, tagLine=118]
Solution: Declare the type via the
localVariableTypes
map parameter.
More Documentation
Check the JavaDoc, especially in JsfStaticAnalyzer.Limitations
- Currently only local variables defined by h:dataTable's var are recognized. To add support for others you'd need create and register a class similar to DataTableVariableResolver
- Handling of included files isn't perfect, the don't know about local variables defined in the including file. But we have all info needed to implement this. Static includes are handled by the Jasper parser (though it likely parses the included files also as top-level files, if they are on its search path).
Future
It depends on my project's needs, your feedback and your contributions :-).Where to Get It
From the project's GitHub or from the project's Maven Central repository, snapshots also may appear in the Sonatype snapshots repo.Appendices
A. Dependencies of v.0.9.0 (also mostly similar for later versions):
(Note: Spring is not really needed if you haven't Spring-managed JSF beans.)aopalliance:aopalliance:jar:1.0:compile commons-beanutils:commons-beanutils:jar:1.6:compile commons-collections:commons-collections:jar:2.1:compile commons-digester:commons-digester:jar:1.5:compile commons-io:commons-io:jar:1.4:compile commons-logging:commons-logging:jar:1.0:compile javax.faces:jsf-api:jar:1.1_02:compile javax.faces:jsf-impl:jar:1.1_02:compile org.apache.tomcat:annotations-api:jar:6.0.29:compile org.apache.tomcat:catalina:jar:6.0.29:compile org.apache.tomcat:el-api:jar:6.0.29:compile org.apache.tomcat:jasper:jar:6.0.29:compile org.apache.tomcat:jasper-el:jar:6.0.29:compile org.apache.tomcat:jasper-jdt:jar:6.0.29:compile org.apache.tomcat:jsp-api:jar:6.0.29:compile org.apache.tomcat:juli:jar:6.0.29:compile org.apache.tomcat:servlet-api:jar:6.0.29:compile org.mockito:mockito-all:jar:1.8.5:compile org.springframework:spring-beans:jar:2.5.6:compile org.springframework:spring-context:jar:2.5.6:compile org.springframework:spring-core:jar:2.5.6:compile xml-apis:xml-apis:jar:1.0.b2:compile