Complex validation of CQ5 dialog fields using a custom Sling servlet

Monday, April 16, 2012

Dialog fields in CQ5 occasionally require validation that cannot be handled with static constraints or regular expressions. However, this type of validation can be easily accomplished by delegating the validation function to a custom servlet. The implementation details and code samples below describe a viable solution for most validation scenarios.

Abstract Validation Servlet

A project may require several dialog validators, so it makes sense to abstract common functionality into a base class. This class is excerpted from the CITYTECH CQ5 library, which we offer to clients as a foundation package for new CQ5 projects.

import java.io.IOException;
import java.util.Collections;

import javax.servlet.ServletException;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonGenerator.Feature;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractValidatorServlet extends SlingSafeMethodsServlet {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractValidatorServlet.class);

    private static final JsonFactory FACTORY = new JsonFactory().disable(Feature.AUTO_CLOSE_TARGET);

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    protected final void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
        throws ServletException, IOException {
        final String value = request.getRequestParameter("value").getString();
        final String path = request.getResource().getPath();

        final boolean valid = isValid(request, path, value);

        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        try {
            final JsonGenerator generator = FACTORY.createJsonGenerator(response.getWriter());

            MAPPER.writeValue(generator, Collections.singletonMap("valid", valid));
        } catch (final JsonGenerationException jge) {
            LOG.error("error generating JSON response", jge);
        } catch (final JsonMappingException jme) {
            LOG.error("error mapping JSON response", jme);
        } catch (final IOException ioe) {
            LOG.error("error writing JSON response", ioe);
        }
    }

    /**
     * Validate the given value for this request and path.
     *
     * @param request servlet request
     * @param path path to current component being validated
     * @param value input value to validate
     * @return true if value is valid, false otherwise
     */
    protected abstract boolean isValid(final SlingHttpServletRequest request, final String path, final String value);
}

Dialog- or field-specific Validation Servlet

This sample validator is responsible for ensuring that form names on a given page are unique. Notice that the current component path is passed to the validation method, which provides the necessary context to ensure that the validation function can ignore it's own value when considering the uniqueness of the form names.

Additionally, the servlet implementation uses Apache Felix SCR annotations to create the required OSGi bundle metadata at via the Maven SCR plugin:

  • sling.servlet.resourceTypes : FormsConstants.RT_FORM_BEGIN - register this servlet for the resource type of the component being validated
  • sling.servlet.methods : GET - handle only HTTP GET requests
  • sling.servlet.selectors : validator - trigger this servlet only when the "validator" selector is present
  • sling.servlet.extensions : json - return a JSON response
import java.util.HashMap;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.wcm.foundation.forms.FormsConstants;

@Component
@Service
@Properties({
    @Property(name = "sling.servlet.resourceTypes", value = FormsConstants.RT_FORM_BEGIN),
    @Property(name = "sling.servlet.extensions", value = "json"),
    @Property(name = "sling.servlet.methods", value = "GET"),
    @Property(name = "sling.servlet.selectors", value = "validator"),
    @Property(name = "service.description", value = "Form Validator Servlet")
})
public final class FormValidatorServlet extends AbstractValidatorServlet {

    private static final Logger LOG = LoggerFactory.getLogger(FormValidatorServlet.class);

    @Override
    protected boolean isValid(final SlingHttpServletRequest request, final String path, final String value) {
        final Map<String, String> names = getFormNames(request);

        // ensure that form name is unique among all forms defined on this page
        // (except for itself)
        return !names.containsKey(value) || names.get(value).equals(path);
    }

    private Map<String, String> getFormNames(final SlingHttpServletRequest request) {
        final Map<String, String> names = new HashMap<String, String>();

        final Node currentNode = request.getResource().adaptTo(Node.class);

        try {
            final Node par = currentNode.getParent();

            final NodeIterator nodes = par.getNodes();

            // get all form names for the current paragraph system
            while (nodes.hasNext()) {
                final Node node = nodes.nextNode();

                if (isFormStart(node)) {
                    final String name = node.getProperty(FormsConstants.ELEMENT_PROPERTY_NAME).getString();

                    names.put(name, node.getPath());
                }
            }
        } catch (final RepositoryException re) {
            LOG.error("error getting form names", re);
        }

        return names;
    }

    private boolean isFormStart(final Node node) throws RepositoryException {
        return node.hasProperty(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY)
            && FormsConstants.RT_FORM_BEGIN.equals(node.getProperty(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY).getString())
            && node.hasProperty(FormsConstants.ELEMENT_PROPERTY_NAME);
    }
}

CQ5 Dialog XML

The name field element includes a "validator" attribute, which defines the function used to call the validation servlet and handle the JSON response. The return value is either "true" (Boolean) or an error message to be displayed to the content author.

<?xml version="1.0" encoding="UTF-8"?><jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="cq:Dialog" xtype="dialog">
    <items jcr:primaryType="cq:WidgetCollection">
        <tabs jcr:primaryType="cq:TabPanel">
            <items jcr:primaryType="cq:WidgetCollection">
                <first jcr:primaryType="nt:unstructured" title="Form" xtype="panel">
                    <items jcr:primaryType="cq:WidgetCollection">
                        <name jcr:primaryType="cq:Widget" fieldLabel="Name" name="./name" xtype="textfield" allowBlank="{Boolean}false"
                            validator="function(value) {
                                var dialog = this.findParentByType('dialog');

                                var url = CQ.HTTP.addParameter(dialog.path + '.validator.json', 'value', value);

                                var result = CQ.HTTP.eval(url);

                                return result.valid ? true : 'Form name already exists on this page.';
                            }" />
                    </items>
                </first>
            </items>
        </tabs>
    </items>
</jcr:root>

See the Gist for this post at GitHub.

Top