Creating a Link Rewriter

Let’s suppose that we have a requirement to apply special CSS style on all external links added via Text&Image component. On this way, external links could get some nice look and feel, like this one.

One solution would be to configure rich text editor of Text&Image component with a custom CSS style and explain content editors about how to use it in order to mark links as external. It could work but content editors could forget to apply it and external links could end up on Publish without the desired styling (so they will look like the internal ones).

So the best way would be to implement some mechanism which will automatically detect external links in Text&Image components and add this specific CSS class to them. The solution is in implementing a custom link transformer.

Introduction

The official documentation says: The Apache Sling Rewriter is a module for rewriting the output generated by a usual Sling rendering process. Some possible use cases include rewriting or checking all links in an HTML page, manipulating the HTML page, or using the generated output as the base for further transformation.

In essence there’s something which is called pipeline. The pipeline is used for post processing of the generated response, and its structure is shown on the following picture:

Sling Rewriter Pipeline
Sling Rewriter Pipeline

The first component in the pipeline is called generator. The generator gets the output from Sling, generates SAX events, and streams these events into the pipeline. The counterpart of the generator is serializer which is at the end of the pipeline. The serializer collects all incomming SAX events, transforms them into the required response by writing into output stream of the response.

Between the generator and the serializer, there’s a chain of transformer instances. A transformer receives SAX events from the previous component in the pipeline and sends SAX events to the next component in the pipeline. A transformer can remove events, change events, add events or just pass on the events.

One thing to remember is that Sling provides a default pipeline which is executed for all HTML responses: it has HTML generator, which parses a HTML output and sends events into the pipeline. HTML serializer collects all events and serializes the output.

A pipeline configurations are stored in the repository under the path /apps/yourapp/config/rewriter and the most notable properties are:

  • generatorType (string, required): type of a generator
  • transformerTypes (multi value string, optional): types of transformers
  • serializerType (string, required): type of a serializer
  • contentTypes (multi value string, optional): response content type, eg. text/html
  • extensions (multi value string, optional): request extension, eg. html
  • order (long): highest wins
  • enabled (boolean)

Fore every component (generator, transformer, serializer) which needs to be added into a pipeline, a configuration node has to be created below a node which represents the pipeline (eg. /apps/yourapp/config/rewriter/some-pipeline). The names of these child nodes have the form <componentType>-<name> (eg. generator-html, transformer-ext-link, etc.).

The only suggested property for these child nodes is component-optional (boolean), although we can add other (custom) properties and use them in pipeline components, which I’m going to show you below.

Implementation

Each pipeline component type has a corresponding Java interface (Generator, Transformer, and Serializer) together with a factory interface (GeneratorFactory, TransformerFactory, and SerializerFactory). When implementing such a component, both interfaces need to be implemented.

In order to solve our task, we need to create a custom link transformer. Therefore we have to implement both Transformer and TransformerFactory interfaces.

Transformer Factory

The sample implementation of the transformer factory would look like this:

import org.apache.sling.rewriter.Transformer;
import org.apache.sling.rewriter.TransformerFactory;
import org.xml.sax.ContentHandler;
// ... other imports

@Component(
label = "External Link Transformer Factory",
description = "Rewrites external links by adding the custom CSS class to them.")
@Service(value = TransformerFactory.class)
public class ExternalLinkTransformerFactory implements TransformerFactory {

@Property(value = "extlink", propertyPrivate = true)
private static final String PROPERTY_PIPELINE_TYPE = "pipeline.type";

@Override
public Transformer createTransformer() {
return new ExternalLinkTransformer();
}

private class ExternalLinkTransformer implements Transformer {
// see below
}

}

The transformer factory implementation has only one method – factory method which produces a concrete transformer instance (line 16). It’s registered as OSGi service under the fully qualified name of TransformerFactory interface (line 9), and it defines the name of the custom transformer (lines 12 and 13) which is used in the pipeline configuration stored in repository. We will see that configuration later.

Transformer

In the code snippet above, the custom transformer implementation is an inner class but this is not mandatory.
The sample implementation of the transformer would look like this:

private class ExternalLinkTransformer implements Transformer {

private static final String TAG_ANCHOR = "a";
private static final String ATTR_CLASS = "class";
private static final String ATTR_TYPE_CDATA = "CDATA";
private static final String ATTR_HREF = "href";
private static final String EXT_LINK_CSS_CLASS_NAME = "cssClassName";

private ContentHandler contentHandler;
private String cssClassName;

@Override
public void init(ProcessingContext processingContext,
ProcessingComponentConfiguration processingComponentConfiguration)
throws IOException {
cssClassName = (String) processingComponentConfiguration
.getConfiguration()
.get(EXT_LINK_CSS_CLASS_NAME);
}

@Override
public void setContentHandler(ContentHandler contentHandler) {
this.contentHandler = contentHandler;
}

@Override
public void dispose() { }

@Override
public void setDocumentLocator(Locator locator) {
contentHandler.setDocumentLocator(locator);
}

@Override
public void startDocument() throws SAXException {
contentHandler.startDocument();
}

@Override
public void endDocument() throws SAXException {
contentHandler.endDocument();
}

@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
contentHandler.startPrefixMapping(prefix, uri);
}

@Override
public void endPrefixMapping(String prefix) throws SAXException {
contentHandler.endPrefixMapping(prefix);
}

@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
contentHandler.startElement(uri, localName, qName, rebuildAttributes(localName, atts));
}

private Attributes rebuildAttributes(String elementName, Attributes currentAttrs) {
if(!TAG_ANCHOR.equalsIgnoreCase(elementName)) {
return currentAttrs;
}

AttributesImpl newAttrs = new AttributesImpl(currentAttrs);
String url = newAttrs.getValue(ATTR_HREF);
if(isExternalLink(url)) {
addCSSClassName(newAttrs);
return newAttrs;
} else {
return currentAttrs;
}
}

private boolean isExternalLink(String url) {
return StringUtils.isNotBlank(url)
&& (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//"));
}

private void addCSSClassName(AttributesImpl newAttrs) {
String classAttrValue = newAttrs.getValue(ATTR_CLASS);
if(StringUtils.isBlank(classAttrValue)) {
newAttrs.addAttribute(StringUtils.EMPTY, ATTR_CLASS, ATTR_CLASS, ATTR_TYPE_CDATA, cssClassName);
} else {
if(!classAttrValue.contains(cssClassName)) {
StringBuilder classAttrValueBuilder = new StringBuilder(classAttrValue);
classAttrValueBuilder.append(StringUtils.SPACE);
classAttrValueBuilder.append(cssClassName);
newAttrs.setAttribute(newAttrs.getIndex(ATTR_CLASS), StringUtils.EMPTY, ATTR_CLASS,
ATTR_CLASS, ATTR_TYPE_CDATA, classAttrValueBuilder.toString());
}
}
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
contentHandler.endElement(uri, localName, qName);
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
contentHandler.characters(ch, start, length);
}

@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
contentHandler.ignorableWhitespace(ch, start, length);
}

@Override
public void processingInstruction(String target, String data) throws SAXException {
contentHandler.processingInstruction(target, data);
}

@Override
public void skippedEntity(String name) throws SAXException {
contentHandler.skippedEntity(name);
}
}

The most of the methods are delegating the calls to the corresponding ContentHandler methods. There are two things to mention here: reading the custom property cssClassName (line 16), and the logic which will add the custom CSS class name (value of cssClassName property) for external links (line 59).

Pipeline configuration

The pipeline configuration is placed below /apps/yourapp/config/rewriter path:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:Folder">
<linkrewriter-pipeline-config
jcr:primaryType="nt:unstructured"
contentTypes="[[text/html]]"
enabled="{Boolean}true"
generatorType="htmlparser"
order="{Long}1"
serializerType="htmlwriter"
transformerTypes="[linkchecker,extlink]">
<transformer-extlink
jcr:primaryType="nt:unstructured"
component-optional="{Boolean}false"
cssClassName="ext-link"/>
</linkrewriter-pipeline-config>
</jcr:root>

In line 14, the custom transformer is added in the chain of transformers and we use the name of the custom transformer which we defined for pipeline type property above (ExternalLinkTransformerFactory#PROPERTY_PIPELINE_TYPE). The same name we use when defining the configuration node for our custom transformer, by following the pattern <componentType>-<name> (line 15). Custom property is defined in line 18 – CSS class name is extracted into the configuration rather than hardcoding it in Java. On this way, CSS class name can be changed in runtime.

CSS definition

At the end, let’s give the sample CSS definition:

.yourapp-content-container .text .ext-link {
/* ... */
}

Here, we’re limiting the desired look and feel on external links located only in Text&Image component. yourapp-content-container is div container of a content section (between header and footer) and text is a CSS class of Text&Image div container. For example, an application can have external links in a navigation menu (usually a part of header section) and it might not be desirable to have the specific look and feel for these external links.

Note: it’s worth mentioning that Sling Rewriter cannot be configured on the way that it rewrites links inside specific content components (eg. Text&Image). Because of that, the solution above is given. resourceTypes parameter does work for page components only. In other words, the rewriter is not invoked onto single components, but only to the output as a whole.

Additional resources

I’d recommend you the following materials to read as well: