#6 AEM Templating with JSTL

Presented by Brendan R.

This episode describes how to simplify AEM presentation templates using JSP tags and JSTL.

Overview

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.

Resources

Here are some references mentioned in the video:

Some things to consider

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:

  • How much error handling do you expect display code to really deal with? Is it worth our time to catch every possible error in display code, or should we let the container handle it?
  • What data is mantatory and what data is optional? Mandatory values should be identified in the code as early as possible. Optional values can be addressed within the display, if it doesn’t make a mess of things.
  • Why are you including other JSPs? Is it to encapsulate and reuse common logic? Is it to break up a big problem into smaller ones? How tight/loosely coupled are these pieces of code? If the answer is “They are tight” or “They depend heavily on each other to work properly” then you should NOT be using includes! I’ll harp more on that point later.
  • Are you repeating the same markup many times in one or more templates? If so, have you considered writing a tag for it? (No, really I’m serious!) Writing a custom JSTL tag does NOT mean you have to write a java class! There are cooler ways of doing this with “tag” files (which are actually special JSP files), and it’s been around for several years! It’s an underdog feature and we’ll explore it at the end.

Examples from the video

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.

button.jsp

<%@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:

dialog.xml

<?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. ;-)

button.css

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.

tabs.jsp

<%@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>

dialog.xml

<?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 &quot;+&quot; 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>

contentHelper.tld

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>

JSTLHelper.java

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! :-)

IncludeTag.java

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);
    }
}