Resolve your NodeData automatically using a @NodeData parameter annotation in your Blossom powered Spring MVC controllers
Resolve all content nodeData values in the signature of your controller method
When we were working on a recent Magnolia project here at Orange11, I introduced Magnolia and Blossom to a colleague of mine. Since Blossom enables the use of Spring MVC, this enables developers to work on Magnolia integration features, without having to know the entire CMS by heart.
One of the first things my colleague did, was addressing content node properties directly in the signature of the controller method.
When you are used to Spring MVC, you are also used to the fact that any relevant parameters in your method signature will automatically be resolved. But unfortunately I had to inform him that we only get the parameter types that come in through the BlossomWebArgumentResolver out-of-the-box.
Here you can read a bit more about Blossom’s argument resolver, as described by Tobias Mattsson, the founder and lead developer of Blossom.
As the words left my mouth, we both realized that this would be a nice add-on for use in Blossom MVC controllers.
So this is why we introduced the method parameter annotation: @NodeData.
The annotation is quite useful in combination with Blossom. The argument resolver will try and resolve all annotated parameters from the current content node out of the aggregationState.
An example of the use of this annotation:
@RequestMapping("/myParagraph") public String getStuffForMyBlossomParagraph( ModelMap modelMap, @NodeData String eventCode, @NodeData(name="startDate") Date date, @NodeData(defaultValue="true") boolean active) { ....
The method signature above would lead in the resolving of the following parameter values out of the current content node:
- String nodeData with name eventCode
- Date nodeData with name startDate
- boolean nodeData with name active, that defaults to true when no value is found
This is a very standard thing to do with Spring, but the results can be really powerful and makes the controller in combination with your content very easy to use. Also the mocking of your controller will become that much easier, because you won’t need to extract any data from a Content node anymore, but just pass the value directly as a method parameter.
A full tutorial and zip with sourcecode after the jump.
As I mentioned, creating an argumentResolver is quite trivial. But the results of such an implementation can be very useful. All that is needed for this case is:
- The NodeData annotation itself
- An argument resolver that resolves the actual nodeData values
- Register the argument resolver in your spring context
1. The NodeData annotation
We will start off by creating the annotation for this parameter type called @NodeData.
The annotation will be accessible via reflection at runtime, and will apply to method parameters.
The annotation will accept:
- value an alias for the name
- name the name of the nodeData
- defaultValue a default value when no content was found in the node
package nl.orange11.spring.web.bind.annotation /** * Annotation that describes a node data value * * @author erik @ Orange11 * */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface NodeData { /** * Alias for String name() * * @return string */ String value() default ""; /** * The name of the nodeData * * @return string */ String name() default ""; /** * The default value * * @return string */ String defaultValue() default ""; }
2. An argument resolver that resolves the actual nodeData values
Now lets create the argument resolver for Spring that resolves the actual value of the parameter.
The resolver displayed below is split up into four steps / excerpts of code. A link to the complete source in a zip (that will save you some copy paste work) is available at the bottom of this page.
One: Create a class that implements the WebArgumentResolver interface.
package nl.orange11.spring.web.bind; /** * WebArgumentResolver that resolves @NodeData annotated method * parameters to their actual node data values * * @author erik @ Orange11 * */ public class NodeDataArgumentResolver implements WebArgumentResolver { ...
Two: Here we override the resolveArgument method.
We try and get our @NodeData annotation. When the annotation is found, we will call a method for the actual resolving of it’s value.
@Override public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception { NodeData nodeDataAnnotation = methodParameter .getParameterAnnotation(NodeData.class); if (nodeDataAnnotation != null) { return resolveNodeDataValue(methodParameter, nodeDataAnnotation); } return UNRESOLVED; }
Three: Check the name of the nodeData to resolve, of what type the parameter is, and based on that type, call the correct method resolving the correct value. Some code was left out for clarity here.
private Object resolveNodeDataValue(MethodParameter methodParameter, NodeData nodeDataAnnotation) { // set the nodeDataName to the name of the parameter // or when not empty to the value or name of the annotation String nodeDataName = methodParameter.getParameterName(); if (nodeDataAnnotation.value() != null && !nodeDataAnnotation.value().isEmpty()) { nodeDataName = nodeDataAnnotation.value(); } else if (nodeDataAnnotation.name() != null && !nodeDataAnnotation.name().isEmpty()) { nodeDataName = nodeDataAnnotation.name(); } // get the default value. String defaultValue = nodeDataAnnotation.defaultValue(); // get the current content node out of the aggregationState Content currentContent = MgnlContext.getAggregationState() .getCurrentContent(); // handle the different parameter types: Class parameterType = methodParameter.getParameterType(); if (parameterType.equals(String.class)) { return resolveString(defaultValue, currentContent, nodeDataName); } else if (parameterType.equals(Boolean.class) || parameterType.equals(boolean.class)) { return resolveBoolean(defaultValue, currentContent, nodeDataName); } else if ( ... return null; }
Four: Now we need to resolve the different types of nodeData values. Below is an example implementation for resolving String and Boolean values.
private String resolveString(String defaultValue, Content currentContent, String nodeDataName) { // In case of a string: when empty default to null if (defaultValue != null && defaultValue.isEmpty()) { defaultValue = null; } return NodeDataUtil.getString(currentContent, nodeDataName, defaultValue); } private Boolean resolveBoolean(String defaultValue, Content currentContent, String nodeDataName) { // In case of a boolean: when empty default to false if ((defaultValue != null && defaultValue.isEmpty()) || defaultValue == null) { defaultValue = Boolean.FALSE.toString(); } return NodeDataUtil.getBoolean(currentContent, nodeDataName, Boolean.valueOf(defaultValue)); } ...
This is only an example of how to implement an argument resolver for your nodeData values. The implementation for resolving all of the other node types is also very trivial and more of the same, so left out for clarity, but available in a download at the bottom of this page.
3. Register the argument resolver in your spring context
Now that everything is in place, the only thing you would need to do here, is add your custom argument resolver to Springs’ AnnotationMethodHandlerAdapter.
Locate the bean with class:
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
in your blossom context xml, and add your custom argument resolver bean to the list already containing Blossoms’ BlossomWebArgumentResolver like the example snippet below:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="customArgumentResolvers"> <list> <bean class="info.magnolia.module.blossom.web.BlossomWebArgumentResolver" /> <bean class="nl.orange11.spring.web.bind.NodeDataArgumentResolver" /> </list> </property> </bean>
All done!
When this last step is complete, it’s all done, and you can start using the @NodeData annotation in your Blossom controllers!
If and when you do implement this, do let us know how you get on, we’re always interested to hear about your experiences.
You can find the complete source described in this post here in a zip file.