- Andrew Kos
- Bill Burlein
- Bryan Williams
- Christian Vozar
- Jeff Brown
- John Kraus
- Joseph Mak
- Josh Durbin
- Mark Daugherty
- Matt Van Bergen
- Melissa Geoffrion
- Michael Kang
- Michael Chan
- Michael Hodgdon
- Mike Motherway
- Molly McDaniel
- Nadia Maciulis
- Pat McLoughlin
- Paul Michelotti
- Puru Hemnani
- Rohit Srinath
- Ryan Lunka
- Tom Kelly
All Blogs
CITYTECH Blogroll:
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.
Mark Daugherty
A longtime enthusiast of logic and problem solving, it seems appropriate that Mark would be inspired by software ideas and technologies...
Recent Posts
- Descriptive JMX Beans in AEM/CQ
- Invisible requirements within Business requirements
- Building a better Options Predicate
- Javascript, This, and You.
- Extensionless URLs with Adobe Experience Manager
- The Life of a Tester in Adobe CQ World!
- Limitations of the CQ Parsys Model and the Implementation of a Nested Paragraph System
- Google Analytics and AEM: No JavaScript? No Problem.
- Using Apache FOP to generate a PDF document based on a form submission data
- Configuring SAML in AEM 5.6