Creation date: 2012-06-25T00:28:14
Dev: Why do our CQ components turn into spaghetti the moment we touch JSPs?
Me: Because we cram content logic, presentation, and null checks in the same file. We can do better. Picture clean templates that only show markup. Picture simple Java classes that hold the data. Call those classes Sling models. And picture a simple HTML friendly templating style. Call that HTL in spirit. We can build toward that today with Sling and CQ.
Why this matters
JSP scriptlets bleed into every corner of a site once a project gets busy. Teams move fast, deadlines get tight, and the small if checks and format calls pile up. The fallout is predictable:
- Hard to read. Markup and logic fight for space. New teammates spend more time parsing than shipping.
- Duplicate code. Title truncation, link building, image fallbacks copied from component to component.
- Risky edits. A safe looking change in a JSP introduces a null pointer or a slow lookup in production.
- Tight coupling. Templates know too much about the repository structure and service details.
We already have the tools in CQ5 and Sling to fix this without waiting for a new framework. Sling adaption, ValueMap, and OSGi services give us clean layers. We just need to agree on a pattern.
The proof on a napkin
Here is a tiny teaser component done the usual way inside a JSP. It works. It also invites trouble.
<%@page session="false" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String title = properties.get("jcr:title", "");
String link = properties.get("link", "");
if (link == null || link.length() == 0) {
link = currentPage.getPath();
}
String image = properties.get("image", "/etc/designs/site/default.png");
if (title == null || title.length() == 0) {
title = currentPage.getTitle();
}
%>
<div class="teaser">
<a href="<%= xssAPI.encodeForHTMLAttr(link) %>">
<img src="<%= xssAPI.encodeForHTMLAttr(image) %>" alt="<c:out value='${currentPage.title}' />" />
<h3><c:out value="<%= title %>" /></h3>
</a>
</div>Now compare that with a split where a model prepares the data and the JSP only renders HTML. The JSP stays calm and readable, and we can unit test the model without a servlet container.
// api/Teaser.java
package com.example.site.api;
public interface Teaser {
String getTitle();
String getLink();
String getImage();
boolean isEmpty();
}// core/TeaserModel.java
package com.example.site.core;
import com.example.site.api.Teaser;
import org.apache.sling.api.adapter.Adaptable;
import org.apache.sling.api.adapter.AdapterFactory;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.osgi.service.component.annotations.Component;
@Component(
service = AdapterFactory.class,
property = {
AdapterFactory.ADAPTABLE_CLASSES + "=org.apache.sling.api.resource.Resource",
AdapterFactory.ADAPTER_CLASSES + "=com.example.site.api.Teaser"
}
)
public class TeaserModel implements AdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <AdapterType> AdapterType getAdapter(Adaptable adaptable, Class<AdapterType> type) {
if (type == Teaser.class && adaptable instanceof Resource) {
Resource r = (Resource) adaptable;
ValueMap vm = r.getValueMap();
return (AdapterType) new TeaserBean(
safe(vm.get("jcr:title", String.class), r.getName()),
safe(vm.get("link", String.class), r.getPath()),
safe(vm.get("image", String.class), "/etc/designs/site/default.png")
);
}
return null;
}
private String safe(String val, String fallback) {
return val != null && val.length() > 0 ? val : fallback;
}
private static final class TeaserBean implements Teaser {
private final String title;
private final String link;
private final String image;
private TeaserBean(String title, String link, String image) {
this.title = title;
this.link = link;
this.image = image;
}
public String getTitle() { return title; }
public String getLink() { return link; }
public String getImage() { return image; }
public boolean isEmpty() { return title == null || title.length() == 0; }
}
}<%@page session="false" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
com.example.site.api.Teaser teaser = resource.adaptTo(com.example.site.api.Teaser.class);
%>
<c:if test="${not empty teaser and not teaser.empty}">
<div class="teaser">
<a href="<%= xssAPI.encodeForHTMLAttr(teaser.getLink()) %>">
<img src="<%= xssAPI.encodeForHTMLAttr(teaser.getImage()) %>" alt="<c:out value='${currentPage.title}' />" />
<h3><c:out value="<%= teaser.getTitle() %>" /></h3>
</a>
</div>
</c:if>The JSP is now only markup and simple getters. The data prep moved into a class that can be reused by other components. That is the spirit of Sling models. The template is mostly HTML with small expressions. That aims for a clean HTL style.
How I wired it in CQ5
- Create an API. Define tiny interfaces for what the template needs. Keep names concrete. Teaser, Card, NavItem.
- Adapt resources. Register an AdapterFactory from Resource to your API. Read from ValueMap. Wrap lookups like image fallbacks and link building in the model.
- Keep services out of JSPs. If a model needs a search or a link mapper, inject an OSGi service inside the model class. Do not call services from the JSP.
- Limit template logic. Allow simple if checks and loops. No repository access. No string gymnastics.
- Cache smartly. If a model is expensive, memoize inside the model and listen to changes with a ResourceChangeListener to drop entries.
This fits nicely with Sling. A resource adapts to a model. The model hides details and exposes simple getters. The template only reads values. That is it.
Risks and tradeoffs
- Over modeling. Keep models small. If you feel the need to pass ten values to a template, split the component.
- Classloader surprises. In OSGi you deploy bundles. If your AdapterFactory does not register, check the service properties and package exports. Watch the logs on bundle start.
- Hidden work. Putting logic in models is great, but track cost. A model that does a query on every request will bite you. Add simple timings in logs while you tune.
- Team habits. Old habits pull logic back into JSPs. Enforce code reviews that flag scriptlets doing too much.
- Testing blind spots. Unit test models with plain JUnit. Mock ValueMap and resources. Add a couple of rendering tests with HtmlUnit or a lightweight client to catch template mistakes.
A tiny helper tag to remove the last scriptlet
If you want to avoid the one scriptlet that adapts the resource, add a very small custom tag. Then your JSP uses only EL.
// tags/AdaptTag.java
package com.example.site.tags;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import java.io.IOException;
import org.apache.sling.api.resource.Resource;
public class AdaptTag extends SimpleTagSupport {
private String var;
private String type;
public void setVar(String var) { this.var = var; }
public void setType(String type) { this.type = type; }
@Override
public void doTag() throws JspException, IOException {
PageContext pc = (PageContext) getJspContext();
Resource r = (Resource) pc.getRequest().getAttribute("org.apache.sling.api.resource.Resource");
try {
Class<?> clazz = Class.forName(type);
Object model = r.adaptTo(clazz);
pc.setAttribute(var, model);
} catch (ClassNotFoundException e) {
throw new JspException(e);
}
}
}<%@taglib prefix="x" uri="/WEB-INF/tlds/xcq.tld" %>
<x:adapt var="teaser" type="com.example.site.api.Teaser" />
<c:if test="${not empty teaser and not teaser.empty}">
<div class="teaser">
<a href="${teaser.link}">
<img src="${teaser.image}" alt="${currentPage.title}" />
<h3>${teaser.title}</h3>
</a>
</div>
</c:if>Now the template reads like HTML with light expressions. That is the spirit of a cleaner template language. Call it HTL in your head if that helps frame the goal.
What this buys you
- Predictable templates. Designers read them. Reviewers spot issues quickly.
- Reusable Java. Models get shared across components. No more copy paste chains.
- Fewer bugs. Null checks and link rules live in one place.
- Easier tests. You can test models without a container, and you can test templates with a simple HTML checker.
Graceful exit
If your CQ codebase feels heavy, move logic into Sling backed models and keep templates clean. Start with one component. Write the tiny interface, wire an AdapterFactory, and strip the JSP down to HTML plus getters. After two or three components you will feel the codebase breathe again.
We do not need a new framework to get there. Sling already gives enough hooks. Build toward clean templates now and your future self will thank you.