writing mvc framework in cq4 (part 3)
In the first part of this little tutorial I’ve shown three major problems I encountered when building a CQ 4 framework. Two of them were solved in the previous post, leaving the persistence one for today. Once again, what it’s about: let’s assume you’ve got your shiny new project with Java controller and JSP view, the data you’re about to use to create your model are stored in pages as a set of atoms, containers and container lists. The simplest way of accessing them is by using e.g.:
Container pageContent = page.getContent(); ContainerList list = pageContent.getContainerList("myData"); Container content = list.getContainer("Single"); Atom atom = content.getAtom("myAtom"); String data = atom.getString();
Though it’s scary, it works just fine. Though it works, it’s too damn easy to hide an error here. For pages storing multiple containers with many atoms inside, managing such a mapping has proven to be really tricky.
We at Cognifide have been struggling with this one for quite a while. The credit for the solution I am about to present goes mainly to Albert Cenkier who invented an automated mapper from a CQ container to a Java object. In order to build it we did the following assumptions:
- one CQ container is mapped to one java object (DTO),
- an atom is mapped to the object field of the same name as atom’s label (case insensitive),
- the above requires that all labels used are valid Java identifiers - we use camel case names.
The mechanism we came up with uses reflection mechanism to find all the properties of a DTO class. It then tries to fetch atoms with the same labels as the fields from the given container. This approach is called “standard over configuration” - instead of the excessive layer enabling the most flexible and elaborate mapping (that you’re not ever going to use anyway) it introduces a standard solution at the cost of a few restrictions (that you most often abide to already).
Time for more implementation details. Here are some example code listings - not a complete solution, but should be enough to get you started. First - get all setters of a given class:
private static Collection/* <Method> */getSetters(Class clazz) { Method[] methods = clazz.getMethods(); Pattern pattern = Pattern.compile("(set)([A-Z_]\\w+)"); Collection/* <Method> */setters = new ArrayList/* <Method> */(); for (int i = 0; i < methods.length; i++) { String methodName = methods[i].getName(); if (matcher.matches()) { setters.add(methods[i]); } } }
Now, to get those from a container and execute the setter:
public static Object mapContainerToObject(Container content, Collection/* <Method> */setters) throws Exception { Pattern pattern = Pattern.compile("(get|set)([A-Z_]\\w+)"); Object result = clazz.newInstance(); for (int i = 0; i < setters.size(); i++) { Method setter = (Method) setters.get(i); Matcher matcher = pattern.matcher(method.getName()); String propertyName = matcher.group(2); propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1); if (content.hasElement(propertyName)) { String value = content.getAtom(propertyName).getString(); setter.invoke(result, new Object[] { value }); } } }
Note - the above doesn’t implement case insensitivity and assumes that every value is a string. The implementation of those complicates the code significantly and I wanted to show the basic idea here. From this point however, it shouldn’t be much of a challenge to implement the following:
- mapping to any basic data type (string, integer, boolean, floating-point, date, etc.) based on Java field type and pre-defined parsing rules,
- mapping container label to one specific field (essential for list’s parNum),
- mapping a container list to a list of objects.
In fact, it is possible to implement a complete hibernate-like solution for Communiqué. Just remember - do not over-complicate. This piece of code lies at the very basis of all your projects. If it’s clean and simple, anything built on it will be so as well.
