This episode describes how to simplify AEM presentation templates using JSP tags and JSTL.
Breaking old habits can be difficult, especially when those habits appear in many training manuals and common code samples. Still, it is possible to gut most – if not all – scriptlet code from a JSP template and wind up with something that is not only equvilent in function, but also much shorter and more readable.
JSTL is not for everyone. It feels bulky. It requires more thought to do things that you already know how to do in Java with less effort. With a few good examples to guide you, I assure you that most of these pain points go away after a while. Since JSTL is neither new nor obscure, there have been many articles about its merits both good and bad. I’ll leave the weighing of pros and cons to the articles written by others, but I will simply add this: JSTL and EL expressions in CQ work the same as they do in standard web application development.
Here are some references mentioned in the video:
Before diving into any examples I must divulge some bad news: The JCR API is not very friendly to JSTL or EL expressions. In fact, things that are quite easy to do with “java-bean” based APIs are not even remotely possible in CQ, at least not without a little nudge in the right direction. This is an important thing to keep in mind because there are competitor offerings that have already addressed this pain point. Most of the reason why bad JSP habits continue to perpetuate is because it’s not trivial to wedge JSTL in where it doesn’t seem fit.
Now here’s the good news: This is a very easy problem to solve, and depending on what you’re trying to do it doesn’t require all that much code either. There are some general rules you should follow that can help you avoid some common pitfalls:
The first example is pretty straightforward, and the only thing to see here is that you can access request parameters via the pre-defined “param” variable. For example ${param.name} will output the request parameter “name” if it is provided, or a null if it is not defined.
The second example is where things get a little more interesting. You will have to change the global.jsp location, but as long as your global.jsp includes the cq:defineObjects tag somewhere down the line then the properties map will exist for this JSP to use.
<%@include file="/apps/lq/lq-core/components/pages/global.jsp"%>
<c:set var="url">
<c:choose>
<c:when test="${properties.linkType == 'internal'}">
${properties.internalLink}.html
</c:when>
<c:when test="${properties.linkType == 'external'}">
${properties.externalLink}
</c:when>
</c:choose>
</c:set>
<c:choose>
<c:when test="${not empty url and not empty properties.altText}">
<a href="${url}"
data-toggle="tooltip"
title="${properties.altText}"
class="${properties.buttonStyle}"
${properties.newWindow == 'yes' ? 'target="_blank"':''} >
${properties.text}
</a>
</c:when>
<c:when test="${not empty url and empty properties.altText}">
<a href="${url}"
class="${properties.buttonStyle}"
${properties.newWindow == 'yes' ? 'target="_blank"':''} >
${properties.text}
</a>
</c:when>
<c:otherwise>
<%=i18n.get("Please configure button")%>
</c:otherwise>
</c:choose>
Not shown in the video are the other two things you need to get this component working:
<?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"
autoHeight="{Boolean}true"
minWidth="600"
width="600">
<items
jcr:primaryType="cq:TabPanel"
height="300"
minHeight="300">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Panel"
title="Button properties">
<items jcr:primaryType="cq:WidgetCollection">
<buttonText
jcr:primaryType="cq:Widget"
allowBlank="false"
defaultValue="Button Text"
fieldLabel="Button Text"
name="./text"
width="180"
xtype="textfield"/>
<tabAltText
jcr:primaryType="cq:Widget"
allowBlank="true"
defaultValue="Alt Text"
fieldLabel="Alt Text"
name="./altText"
width="180"
xtype="textfield"/>
<linkType
jcr:primaryType="cq:Widget"
allowBlank="false"
defaultValue="internal"
fieldLabel="Link Type"
name="./linkType"
type="select"
width="150"
xtype="selection">
<listeners
jcr:primaryType="nt:unstructured"
loadcontent="function(field, value) {if (!this.getValue()) {return;} if (this.getValue()=='internal') { this.nextSibling().enable(); this.nextSibling().show(); this.nextSibling().nextSibling().disable(); this.nextSibling().nextSibling().hide(); } else { this.nextSibling().disable(); this.nextSibling().hide(); this.nextSibling().nextSibling().enable(); this.nextSibling().nextSibling().show(); } }"
selectionchanged="function(field,value) {if (!this.getValue()) {return;} if (this.getValue()=='internal') { this.nextSibling().enable(); this.nextSibling().show(); this.nextSibling().nextSibling().disable(); this.nextSibling().nextSibling().hide(); } else { this.nextSibling().disable(); this.nextSibling().hide(); this.nextSibling().nextSibling().enable(); this.nextSibling().nextSibling().show(); } }"/>
<options jcr:primaryType="cq:WidgetCollection">
<internal
jcr:primaryType="nt:unstructured"
text="Internal Link"
value="internal"/>
<external
jcr:primaryType="nt:unstructured"
text="External Link"
value="external"/>
</options>
</linkType>
<internalLink
jcr:primaryType="cq:Widget"
fieldLabel="Internal Link"
name="./internalLink"
rootPath="/content/lq"
width="180"
xtype="pathfield"/>
<externalLink
jcr:primaryType="cq:Widget"
fieldLabel="External Link"
name="./externalLink"
value="http://"
width="180"
xtype="textfield"/>
<newWindow
jcr:primaryType="cq:Widget"
fieldDescription="Open link in new window?"
fieldLabel="New Window?"
inputValue="yes"
name="./newWindow"
type="checkbox"
xtype="selection"/>
<buttonStyle
jcr:primaryType="cq:Widget"
allowBlank="false"
defaultValue="blueButton"
fieldLabel="Button Style"
name="./buttonStyle"
type="select"
value="blueButton"
width="150"
xtype="selection">
<options jcr:primaryType="cq:WidgetCollection">
<blue
jcr:primaryType="nt:unstructured"
text="Blue (default)"
value="blueButton"/>
<orange
jcr:primaryType="nt:unstructured"
text="Orange"
value="orangeButton"/>
<green
jcr:primaryType="nt:unstructured"
text="Green"
value="greenButton"/>
<orangewithoutgradient
jcr:primaryType="nt:unstructured"
text="Orange without Gradient"
value="orangeButtonWithoutGradient"/>
</options>
</buttonStyle>
</items>
</tab1>
</items>
</items>
</jcr:root>
And finally here is the CSS required to drive the look/feel. It has no hope of working 100% correctly in IE, don’t ask. ;-)
a.buttonBase,a.blueButton,input.blueButton,a.orangeButton,a.greenButton,a.orangeButtonWithoutGradient,input.orangeButtonWithoutGradient,a.greenButtonWithoutGradient,input.greenButtonWithoutGradient
{
font-family: vinylregular;
font-size: 1.1em;
display: inline-block;
text-decoration: none;
margin-bottom: 5px;
color: white;
text-shadow: none;
padding: 4px 6px 4px 6px;
border: 2px solid white;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px; /* future proofing */
-khtml-border-radius: 3px; /* for old Konqueror browsers */
-webkit-box-shadow: 0px 2px 5px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0px 2px 5px 1px rgba(0, 0, 0, 0.25);
-ms-filter:
"progid:DXImageTransform.Microsoft.Shadow(Strength=3, Direction=180, Color='#000000')";
}
a.largerButton,input.largerButton {
font-size: 1.3em !important;
}
a.blueButton,input.blueButton {
background: #8fb4ea; /* Old browsers */
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
background:
url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzcyYTFkNyIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM5OGI5ZjAiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
background: -moz-linear-gradient(top, #8fb4ea 0%, #5585d7 100%);
/* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #8fb4ea),
color-stop(100%, #5585d7)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #8fb4ea 0%, #5585d7 100%);
/* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #8fb4ea 0%, #5585d7 100%);
/* Opera 11.10+ */
background: -ms-linear-gradient(top, #8fb4ea 0%, #5585d7 100%);
/* IE10+ */
background: linear-gradient(to bottom, #8fb4ea 0%, #5585d7 100%);
/* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#8fb4ea',
endColorstr='#5585d7', GradientType=0); /* IE6-8 */
}
a.orangeButton {
background: #ffa200; /* Old browsers */
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
background:
url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZlZDk0MCIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNkMTZhMTEiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
background: -moz-linear-gradient(top, #ffa200 0%, #d16a11 100%);
/* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffa200),
color-stop(100%, #d16a11)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffa200 0%, #d16a11 100%);
/* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffa200 0%, #d16a11 100%);
/* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffa200 0%, #d16a11 100%);
/* IE10+ */
background: linear-gradient(to bottom, #ffa200 0%, #d16a11 100%);
/* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffa200',
endColorstr='#d16a11', GradientType=0); /* IE6-8 */
}
a.orangeButtonWithoutGradient,input.orangeButtonWithoutGradient {
background: #ffa200;
}
a.greenButtonWithoutGradient,input.greenButtonWithoutGradient {
background: #8ec000;
font-family: arial;
font-weight: bold;
font-size: 1em;
padding: 1px 6px 1px 6px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
border-radius: 0px; /* future proofing */
-khtml-border-radius: 0px; /* for old Konqueror browsers */
}
a.greenButton {
background: #a8d159; /* Old browsers */
/* IE9 SVG, needs conditional override of 'filter' to 'none' */
background:
url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2E4ZDE1OSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM4OWMxMjAiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);
background: -moz-linear-gradient(top, #a8d159 0%, #89c120 100%);
/* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #a8d159),
color-stop(100%, #89c120)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #a8d159 0%, #89c120 100%);
/* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #a8d159 0%, #89c120 100%);
/* Opera 11.10+ */
background: -ms-linear-gradient(top, #a8d159 0%, #89c120 100%);
/* IE10+ */
background: linear-gradient(to bottom, #a8d159 0%, #89c120 100%);
/* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a8d159',
endColorstr='#89c120', GradientType=0); /* IE6-8 */
}
The third example has a lot more going on, but keep in mind that much of its UI functionality is built around the twitter bootstrap library. You have to have the javascript and css to support bootstrap’s tabbed navigation for this to work.
<%@include file="/apps/lq/lq-core/components/pages/global.jsp"%>
<%@taglib prefix="lq" uri="http://www.lq.com/taglibs/helper/1.0" %>
<c:set var="tabNum" value="${lq:generateId()}"/>
<c:set var="tabs" value="${lq:getNode(currentNode, 'tabEntries')}"/>
<c:choose>
<c:when test="${not empty tabs}">
<div class="tabbable global-tabs ${properties.tabStyle}">
<div class="tab-panel">
<c:if test="${properties.tabStyle == 'vertical'}">
<lq:include path="tabset_header" resourceType="lq/lq-com/components/global/section-header" merge="false">
<tabset_header
sectionHeaderText="Find a hotel"
headerSize="h2"/>
</lq:include>
</c:if>
<ul class="nav nav-tabs" id="tabs_${tabNum}" data-tabs="tabs">
<c:forEach var="entry" items='${tabs.nodes}' varStatus="status">
<c:set var="tabName" value="${lq:getPropertyString(entry,'tabName')}"/>
<c:set var="tabAltText" value="${lq:getPropertyString(entry,'tabAltText')}"/>
<li ${status.first ? "class='active'": ""}>
<a href="#${entry.name}_${tabNum}" alt="${tabAltText}" data-toggle="tab">
${empty tabName ? "No Name" : tabName}
</a>
</li>
</c:forEach>
</ul>
<c:if test="${properties.tabStyle == 'vertical'}">
<div class="gutter">
<cq:include path="gutter" resourceType="foundation/components/parsys"/>
</div>
</c:if>
</div>
<div class="tab-content">
<c:forEach var="entry" items="${tabs.nodes}" varStatus="status">
<div class="tab-pane ${status.first ? 'active':''}" id="${entry.name}_${tabNum}">
<c:if test="${properties.tabStyle == 'vertical'}">
<c:set var="tabName" value="${lq:getPropertyString(entry,'tabName')}"/>
<c:set var="tabAltText" value="${lq:getPropertyString(entry,'tabAltText')}"/>
<h2>${not empty tabAltText ? tabAltText : tabName}</h2>
</c:if>
<cq:include path="${entry.name}" resourceType="foundation/components/parsys"/>
</div>
</c:forEach>
</div>
<div class="clear"></div>
</div>
</c:when>
<c:otherwise>
<%=i18n.get("Please configure Tabs")%>
</c:otherwise>
</c:choose>
<?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"
autoHeight="{Boolean}true"
minWidth="600"
width="600">
<items
jcr:primaryType="cq:TabPanel"
height="300"
minHeight="300">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Panel"
title="Tabs">
<items jcr:primaryType="cq:WidgetCollection">
<tabStyle
jcr:primaryType="cq:Widget"
allowBlank="false"
defaultValue="horizontal"
fieldLabel="Tab Style"
name="./tabStyle"
type="select"
value="horizontal"
width="150"
xtype="selection">
<options jcr:primaryType="cq:WidgetCollection">
<horizontal
jcr:primaryType="nt:unstructured"
text="Horizontal (default)"
value="horizontal"/>
<horizontalforHotelInfo
jcr:primaryType="nt:unstructured"
text="Horizontal (For Hotel Info)"
value="hotelinfo"/>
<vertical
jcr:primaryType="nt:unstructured"
text="Vertical"
value="vertical"/>
</options>
</tabStyle>
<linkEntries
jcr:primaryType="cq:Widget"
baseName="tab_"
fieldDescription="Click on the "+" button to add a link"
fieldLabel="Tabs"
name="./tabEntries"
xtype="mtmulticompositefield">
<fieldConfigs jcr:primaryType="cq:WidgetCollection">
<tabText
jcr:primaryType="cq:Widget"
allowBlank="false"
defaultValue="Tab Name"
fieldLabel="Tab Name"
name="tabName"
width="180"
xtype="textfield"/>
<tabAltText
jcr:primaryType="cq:Widget"
allowBlank="true"
defaultValue="Alt Text"
fieldLabel="Alt Text"
name="tabAltText"
width="180"
xtype="textfield"/>
<resType
jcr:primaryType="cq:Widget"
ignoreData="{Boolean}true"
name="sling:resourceType"
value="lq/lq-com/components/global/mmfield"
xtype="hidden"/>
</fieldConfigs>
</linkEntries>
</items>
</tab1>
</items>
</items>
</jcr:root>
Note: This goes in a java project under /src/resources/META-INF
<?xml version="1.0" encoding="ISO-8859-1"?>
<taglib xmlns="http://java.sun.com/xml/ns.j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com.xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
<description>LQ.Com Helper Tag Library</description>
<tlib-version>1.0</tlib-version>
<short-name>lq</short-name>
<uri>http://www.lq.com/taglibs/helper/1.0</uri>
<tag>
<description>
Includes a resource rendering into the current page. This is 1:1 the same as the CQ include tag
except that anything defined in the body is interpreted as the default node values if the node
at the specified path does not exist.
</description>
<name>include</name>
<tag-class>
com.lq.aem.lqcom.utils.IncludeTag
</tag-class>
<body-content>scriptless</body-content>
<attribute>
<description>
Whether to flush the output before including the target
</description>
<name>flush</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>boolean</type>
</attribute>
<attribute>
<description>
The path to the resource object to include in the current
request processing. If this path is relative it is
appended to the path of the current resource whose
script is including the given resource. Either resource
or path must be specified. If both are specified, the
resource takes precedences.
</description>
<name>path</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<description>
The resource type of a resource to include. If the resource
to be included is specified with the path attribute,
which cannot be resolved to a resource, the tag may
create a synthetic resource object out of the path and
this resource type. If the resource type is set the path
must be the exact path to a resource object. That is,
adding parameters, selectors and extensions to the path
is not supported if the resource type is set.
</description>
<name>resourceType</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<description>
The jsp script to include.
</description>
<name>script</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<description>
Controls if the component hierarchy should be ignored for script
resolution. If true, only the search paths are respected.
</description>
<name>ignoreComponentHierarchy</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<description>
If true, attributes and sub-nodes will be created if they do
not exist, existing values are not overwritten but missing attributes and
child nodes will be recreated if missing.
If false, defaults are processed in a fail-fast mode
whereby the process stops completely
if there is already a target node in JCR. The default is faluse
</description>
<name>merge</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
<function>
<name>generateId</name>
<function-class>com.lq.aem.lqcom.utils.JSTLHelper</function-class>
<function-signature>int generateId()</function-signature>
</function>
<function>
<name>hasNode</name>
<function-class>com.lq.aem.lqcom.utils.JSTLHelper</function-class>
<function-signature>boolean hasNode(javax.jcr.Node, java.lang.String)</function-signature>
</function>
<function>
<name>getNode</name>
<function-class>com.lq.aem.lqcom.utils.JSTLHelper</function-class>
<function-signature>javax.jcr.Node getNode(javax.jcr.Node, java.lang.String)</function-signature>
</function>
<function>
<name>getNodeByPath</name>
<function-class>com.lq.aem.lqcom.utils.JSTLHelper</function-class>
<function-signature>javax.jcr.Node getNodeByPath(org.apache.sling.api.resource.ResourceResolver, java.lang.String)</function-signature>
</function>
<function>
<name>getPropertyString</name>
<function-class>com.lq.aem.lqcom.utils.JSTLHelper</function-class>
<function-signature>java.lang.String getPropertyString(javax.jcr.Node, java.lang.String)</function-signature>
</function>
</taglib>
package com.lq.aem.lqcom.utils;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
/**
* Static helper functions, mostly designed at simplifying JSTL EL expressions.
* These are all referenced in the jstlHelper.tld tag lib descriptor. This helps
* avoid the common exception cases where null defaults are acceptable.
* Repository exceptions will still be thrown if fatal errors occur.
*
* @author brobert
*/
public class JSTLHelper {
/**
* Slightly more convenient than the sling resource resolver function since
* you don't have to turn around and adapt the result to a node
*
* @param resourceResolver
* @param childName
* @return
*/
public static Node getNodeByPath(ResourceResolver resourceResolver, String childName) {
Resource result = resourceResolver.getResource(childName);
if (result == null) {
return null;
}
return result.adaptTo(Node.class);
}
public static boolean hasNode(Node node, String childName) throws RepositoryException {
if (node != null) {
return node.hasNode(childName);
}
return false;
}
public static Node getNode(Node node, String childName) throws RepositoryException {
if (node != null && node.hasNode(childName)) {
return node.getNode(childName);
}
return null;
}
public static String getPropertyString(Node node, String propertyName) throws RepositoryException {
if (node == null || !node.hasProperty(propertyName)) {
return null;
}
Property p = node.getProperty(propertyName);
return p.getString();
}
public static int generateId() {
return (int) Math.round(Math.random() * Integer.MAX_VALUE);
}
}
Oh hey, you’re still here? Here’s a gift from me to you: An extended version of the cq:include tag.
It works exactly the same as the product’s cq:include tag except that you can provide an XML body to it and it will use this as the entire configuration of the component as a set of default values if the component or any of its properties do not exist. This allows you do seed a lot of base content but still let the author edit it freely! :-)
package com.lq.aem.lqcom.utils;
import javax.servlet.jsp.JspException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.JspTag;
import javax.servlet.jsp.tagext.SimpleTag;
import javax.servlet.jsp.tagext.Tag;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.input.ReaderInputStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
/**
* Extends the CQ include tag to offer support for defining default component.
*/
public class IncludeTag implements SimpleTag {
/**
* Proxy object to the original tag, called in tandem. It would be more
* ideal to extend the Include tag, but it is not possible in this case
* because the methods needed to access the JSP Body were marked protected.
*/
com.day.cq.wcm.tags.IncludeTag includeTag = new com.day.cq.wcm.tags.IncludeTag();
@Override
public void doTag() throws JspException, IOException {
Node currentNode = (Node) context.findAttribute("currentNode");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
//Using factory get an instance of document builder
DocumentBuilder db = dbf.newDocumentBuilder();
//parse using builder to get DOM representation of the XML file
StringWriter writer = new StringWriter();
body.invoke(writer);
InputStream is = new ReaderInputStream(new StringReader(writer.toString()));
Document dom = db.parse(is);
Element rootElement = dom.getDocumentElement();
processDefaults(currentNode, rootElement, "nt:unstructured", resourceType);
currentNode.getSession().save();
}
catch (ParserConfigurationException pce) {
throw new JspException("ParserConfigurationException trying to parse Include tag body: " + pce.getMessage(), pce);
}
catch (SAXException se) {
throw new JspException("SaxException trying to parse Include tag body: " + se.getMessage(), se);
}
catch (IOException ioe) {
throw new JspException("IOException trying to parse Include tag body: " + ioe.getMessage(), ioe);
}
catch (RepositoryException ex) {
throw new JspException("RepositoryException trying to process Include tag body: " + ex.getMessage(), ex);
}
includeTag.doEndTag();
includeTag.release();
}
private void processDefaults(Node currentNode, Element element, String jcrType, String slingResourceType) throws RepositoryException {
Node mergeNode = null;
// Create node if it does not exist or merge with existing (if allowed)
if (!currentNode.hasNode(element.getTagName())) {
// Fist determine the jcr type
String type = element.hasAttribute("jcr:primaryType") ? element.getAttribute("jcr:primaryType") : jcrType;
mergeNode = currentNode.addNode(element.getTagName(), type);
String elementType = element.hasAttribute("sling:resourceType") ? element.getAttribute("sling:resourceType") : slingResourceType;
if (elementType != null) {
mergeNode.setProperty("sling:resourceType", elementType);
}
} else {
if (!mergeWithExistingData) {
return;
}
mergeNode = currentNode.getNode(element.getTagName());
}
for (int i = 0; i < element.getAttributes().getLength(); i++) {
org.w3c.dom.Node attr = element.getAttributes().item(i);
String nodeNameRaw = attr.getNodeName();
String nodeName = getNodeName(nodeNameRaw);
String nodeValue = attr.getNodeValue();
if (!mergeNode.hasProperty(nodeName)) {
setNodeProperty(mergeNode, nodeNameRaw, nodeValue);
mergeNode.setProperty(nodeName, nodeValue);
}
}
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
org.w3c.dom.Node child = element.getChildNodes().item(i);
if (!(child instanceof Element)) {
continue;
}
Element childElement = (Element) child;
processDefaults(mergeNode, childElement, jcrType, null);
}
}
private String getNodeName(String nodeNameRaw) {
return nodeNameRaw;
}
@Override
public void setParent(JspTag jsptag) {
includeTag.setParent((Tag) jsptag);
}
@Override
public JspTag getParent() {
return includeTag.getParent();
}
JspContext context;
@Override
public void setJspContext(JspContext jc) {
context = jc;
includeTag.setPageContext((PageContext) jc);
}
JspFragment body;
@Override
public void setJspBody(JspFragment jf) {
body = jf;
}
//-------- Include parameters accepted by CQ Include tag
// These are provided here so that the values can pass through.
// Only the path tag is used by this tag.
public void setFlush(boolean flush) {
includeTag.setFlush(flush);
}
public void setIgnoreComponentHierarchy(boolean ignoreComponentHierarchy) {
includeTag.setIgnoreComponentHierarchy(ignoreComponentHierarchy);
}
public void setPath(String path) {
includeTag.setPath(path);
}
String resourceType;
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
includeTag.setResourceType(resourceType);
}
public void setScript(String script) {
includeTag.setScript(script);
}
boolean mergeWithExistingData;
public void setMerge(boolean merge) {
mergeWithExistingData = merge;
}
private void setNodeProperty(Node node, String propertyName, String propertyValue) throws RepositoryException {
// TODO: Would be more useful if different attribute types could be supported
// Right now everything is treated as string
// Class nodeType = getNodeType(nodeNameRaw);
node.setProperty(propertyName, propertyValue);
}
}