AEM CQ 56 - Extend and Add Filters to Content Finder Page Tab

Goal


In this post we are going to extend the content finder page tab to add additional filters. Ootb, page tab in content finder allows the user to filter results based on a keyword and the search is performed in jcr:content. Here we extend the UI to let user perform search in a specific path, based on title or pages of specific template type. Source code , Package Install and Demo Video are available for download




Prerequisites


If you are new to CQ

1) Read this post on how to create a sample page component

2) Read this post on how to setup your IDE and create an OSGI component

Create the Servlets


1) Code a servlet GetTemplates to return the templates from CQ. When a user clicks on Select Template combo, this servlet returns the available cq:Template in system

package apps.mysample.objectfinder.pagetab;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.servlet.ServletException;
import java.io.IOException;

@SlingServlet(
        paths="/bin/mycomponents/objectfinder/templates",
        methods = "GET",
        metatype = false,
        label = "Get Templates Servlet"
)
public class GetTemplates extends SlingAllMethodsServlet {
    private static final long serialVersionUID = 1L;

    private static final Logger log = LoggerFactory.getLogger(GetTemplates.class);

    @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        JSONWriter jw = new JSONWriter(response.getWriter());
        String template = request.getParameter("query");

        try{
            ResourceResolver resolver = request.getResourceResolver();
            Session session = resolver.adaptTo(Session.class);
            QueryManager qm = session.getWorkspace().getQueryManager();

            String stmt = "//element(*,cq:Template) order by @jcr:title";

            if(StringUtils.isNotEmpty(template)){
                stmt = "//element(*,cq:Template)[jcr:like(fn:upper-case(@jcr:title), '" + template.toUpperCase() + "%')]";
            }

            Query q = qm.createQuery(stmt, Query.XPATH);

            NodeIterator results = q.execute().getNodes();
            Node node = null, tNode = null; String path = null;

            jw.object();
            jw.key("data").array();

            while(results.hasNext()){
                node = results.nextNode();
                path = node.getProperty("jcr:content/sling:resourceType").getString();

                if(path.startsWith("/apps/")){
                    path = path.substring(6);//remove /apps/
                }

                jw.object();
                jw.key("id").value(path);
                jw.key("name").value(node.getProperty("jcr:title").getString());
                jw.endObject();
            }

            jw.endArray();
            jw.endObject();
        }catch(Exception e){
            log.error("Error getting templates",e);
            throw new ServletException(e);
        }
    }
}

2) Code servlet PageResultsViewHandler to peform search when the user selects a filter and clicks on search button of the page tab content finder

package apps.mysample.objectfinder.pagetab;

import com.day.cq.commons.JSONWriterUtil;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.core.contentfinder.Hit;
import com.day.cq.wcm.core.contentfinder.ViewHandler;
import com.day.cq.wcm.core.contentfinder.ViewQuery;
import com.day.cq.xss.ProtectionContext;
import com.day.cq.xss.XSSProtectionException;
import com.day.cq.xss.XSSProtectionService;
import com.day.text.Text;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.jackrabbit.commons.query.GQL;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.day.cq.commons.jcr.JcrConstants.JCR_PATH;

@SlingServlet(
        paths="/bin/mycomponents/objectfinder/pageresults",
        methods = "GET",
        metatype = false,
        label = "Object Finder Page Results Servlet"
)
public class PageResultsViewHandler extends ViewHandler {
    private static final long serialVersionUID = 0L;
    private static Logger log = LoggerFactory.getLogger(PageResultsViewHandler.class);

    private static final String NT_CQ_PAGE = "cq:Page";
    private static final String NT_CQ_PAGE_CONTENT = "cq:PageContent";
    private static final String DEFAULT_START_PATH = "/content";
    private static final String LAST_MOD_REL_PATH = JCR_CONTENT + "/cq:lastModified";

    public static final String PAGE_PATH = "path";
    public static final String TYPE = "type";
    public static final String TEMPLATE = "template";

    /**
     * @scr.reference policy="static"
     */
    private XSSProtectionService xss;

    private String getDefaultIfEmpty(SlingHttpServletRequest request, String paramName, String dValue){
        String value = request.getParameter(paramName);

        if (StringUtils.isEmpty(value)) {
            value = dValue;
        }

        return value.trim();
    }

    @Override
    protected ViewQuery createQuery(SlingHttpServletRequest request, Session session, String queryString)
            throws RepositoryException {
        ParserCallback cb = new ParserCallback();

        boolean isFullTextSearch = getDefaultIfEmpty(request,"fullText", "true").equalsIgnoreCase("true");
        String stmt = null;

        if(isFullTextSearch){
            stmt = queryString;
            GQL.parse(stmt, session, cb);
        }else{
            GQL.parse("", session, cb);
            cb.term("jcr:title", queryString, false);
        }

        StringBuilder gql = cb.getQuery();

        String path = getDefaultIfEmpty(request, PAGE_PATH, DEFAULT_START_PATH);
        path = "path:\"" + path + "\"";

        if (StringUtils.isNotEmpty(gql)) {
            gql.append(" ");
        }

        gql.append(path).append(" ");

        String limit = getDefaultIfEmpty(request, LIMIT, "20");
        limit = "limit:" + limit;

        gql.append(limit).append(" ");

        String template = getDefaultIfEmpty(request,TEMPLATE, "");

        if (isFullTextSearch && StringUtils.isEmpty(queryString) && StringUtils.isEmpty(template)) {
            return new MostRecentPages(request, session, gql, xss);
        }

        String type = getDefaultIfEmpty(request, TYPE, NT_CQ_PAGE);
        type = "type:\"" + type + "\"";
        gql.append(type).append(" ");

        if(StringUtils.isNotEmpty(template)){
            cb.term("sling:resourceType", template, false);
        }

        String order = "order:-" + LAST_MOD_REL_PATH;
        gql.append(order).append(" ");

        return new GQLViewQuery(request, gql.toString(), session, xss);
    }

    private static Hit createHit(Page page, String excerpt, XSSProtectionService xss)
            throws RepositoryException {
        Hit hit = new Hit();
        hit.set("name", page.getName());
        hit.set("path", page.getPath());
        hit.set("excerpt", excerpt);

        if(page.getTitle() != null) {
            hit.set("title", page.getTitle());

            if (xss != null) {
                try {
                    hit.set("title" + JSONWriterUtil.KEY_SUFFIX_XSS, xss.protectForContext(
                            ProtectionContext.PLAIN_HTML_CONTENT,page.getTitle()));
                } catch (XSSProtectionException e) {
                    log.warn("Unable to protect title {}", page.getTitle());
                }
            }
        } else {
            hit.set("title", page.getName());
        }

        if(page.getLastModified() != null) {
            hit.set("lastModified", page.getLastModified());
        }

        return hit;
    }

    private class ParserCallback implements GQL.ParserCallback {
        private StringBuilder query = new StringBuilder();

        public void term(String property, String value, boolean optional)
                throws RepositoryException {
            if(StringUtils.isEmpty(value)){
                return;
            }

            if (optional) {
                query.append("OR ");
            }

            if (StringUtils.isEmpty(property)) {
                query.append("\"jcr:content/.\":\"");
                query.append(value).append("\"");
            } else {
                property = "jcr:content/" + property;
                query.append("\"").append(property).append("\":");
                query.append("\"").append(value).append("\" ");
            }
        }

        public StringBuilder getQuery() {
            return query;
        }
    }

    private static class MostRecentPages implements ViewQuery {
        private final SlingHttpServletRequest request;
        private final Session session;
        private final String gql;
        private final XSSProtectionService xss;

        public MostRecentPages(SlingHttpServletRequest request, Session session, StringBuilder gql,
                               XSSProtectionService xss) {
            this.request = request;
            this.session = session;

            gql.append("type:\"").append(NT_CQ_PAGE_CONTENT).append("\" ");
            gql.append("order:-").append(NameConstants.PN_PAGE_LAST_MOD).append(" ");

            this.gql = gql.toString();
            this.xss = xss;
        }

        public Collection<Hit> execute() {
            List<Hit> hits = new ArrayList<Hit>();
            ResourceResolver resolver = request.getResourceResolver();
            RowIterator rows = GQL.execute(gql, session);

            try {
                while (rows.hasNext()) {
                    Row row = rows.nextRow();
                    String path = row.getValue(JCR_PATH).getString();
                    path = Text.getRelativeParent(path, 1);
                    Resource resource = resolver.getResource(path);

                    if (resource == null) {
                        continue;
                    }

                    Page page = resource.adaptTo(Page.class);
                    if (page == null) {
                        continue;
                    }

                    String excerpt;
                    try {
                        excerpt = row.getValue("rep:excerpt()").getString();
                    } catch (Exception e) {
                        excerpt = "";
                    }

                    hits.add(createHit(page, excerpt, xss));
                }
            } catch (RepositoryException re) {
                log.error("A repository error occurred", re);
            }

            return hits;
        }
    }

    private static class GQLViewQuery implements ViewQuery {
        private final SlingHttpServletRequest request;
        private final String queryStr;
        private final Session session;
        private final XSSProtectionService xss;

        public GQLViewQuery(SlingHttpServletRequest request, String queryStr, Session session, XSSProtectionService xss) {
            this.request = request;
            this.queryStr = queryStr;
            this.session = session;
            this.xss = xss;
        }

        public Collection<Hit> execute() {
            List<Hit> hits = new ArrayList<Hit>();
            ResourceResolver resolver = request.getResourceResolver();

            RowIterator rows = GQL.execute(queryStr, session);
            try {
                while (rows.hasNext()) {
                    Row row = rows.nextRow();
                    String path = row.getValue(JCR_PATH).getString();
                    Page page = resolver.getResource(path).adaptTo(Page.class);

                    String excerpt;
                    try {
                        excerpt = row.getValue("rep:excerpt()").getString();
                    } catch (Exception e) {
                        excerpt = "";
                    }
                    hits.add(createHit(page, excerpt, xss));
                }
            } catch (RepositoryException re) {
                log.error("A repository error occurred", re);
            }
            return hits;
        }
    }
}

Extend Content Finder


The next step is to extend content finder to add UI elements on page tab.

1) Login to CRXDE Lite, create folder (nt:folder) /apps/extendpagetab

2) Create clientlib (type cq:ClientLibraryFolder/apps/extendpagetab/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/extendpagetab/clientlib/js.txt, add the following

                         addfilters.js

4) Create file ( type nt:file ) /apps/extendpagetab/clientlib/addfilters.js, add the following code

CQ.Ext.ns("MyComponents");

MyComponents.ContentFinder = {
    TAB_PAGES : "cfTab-Pages",
    PAGES_QUERY_BOX : "cfTab-Pages-QueryBox",
    CONTENT_FINDER_TAB: 'contentfindertab',
    FULL_TEXT_HIDDEN: "cfTab-Pages-fullText",

    getTemplatesCombo: function(){
        var store = new CQ.Ext.data.Store({
            proxy: new CQ.Ext.data.HttpProxy({
                "autoLoad":false,
                url: "/bin/mycomponents/objectfinder/templates",
                method: 'GET'
            }),
            reader: new CQ.Ext.data.JsonReader({
                root: 'data',
                fields: [
                    {name: 'id', mapping: 'id'},
                    {name: 'name', mapping: 'name'}
                ]
            })
        });

        var combo = {
            store: store,
            hiddenName: "template",
            xtype : "combo",
            "width": "185",
            style: "margin-top:0",
            mode: "remote",
            triggerAction: "all",
            valueField: 'id',
            displayField: 'name',
            emptyText: 'Select or Start Typing',
            minChars: 2
        };

        return combo;
    },

    addPageFilters: function(){
        var tab = CQ.Ext.getCmp(this.TAB_PAGES);
        var queryBox = CQ.Ext.getCmp(this.PAGES_QUERY_BOX);

        queryBox.add({
            id: this.FULL_TEXT_HIDDEN,
            "xtype": "hidden",
            name: "fullText",
            hiddenName: "fullText",
            value: "true"
        });

        var ftHidden = CQ.Ext.getCmp(this.FULL_TEXT_HIDDEN);

        queryBox.add({
            xtype: 'radiogroup',
            style: "margin:10px 0 0 0",
            columns: 1,
            vertical: true,
            items: [{
                boxLabel: ' Full Text',
                name: 'fullTextRB',
                inputValue: '0',
                checked: true
            }, {
                name: 'fullTextRB',
                boxLabel: ' Title',
                inputValue: '1',
                listeners: {
                    'check' : function(c){
                        ftHidden.setValue(!c.checked);
                    }
                }
            }]
        });

        queryBox.add({
            "xtype": "label",
            "text": "Select Path",
            "cls": "x-form-field x-form-item-label"
        });

        var pathField = {
            "xtype": "pathfield",
            "width": "100%",
            "style" : "margin-top: 0px;",
            hiddenName: "path"
        };

        queryBox.add(pathField);

        queryBox.add({
            "xtype": "label",
            "text": "Select Template",
            style: "margin:10px 0 0 0",
            "cls": "x-form-field x-form-item-label"
        });

        queryBox.add(this.getTemplatesCombo());

        var cfTab = queryBox.findParentByType(this.CONTENT_FINDER_TAB);

        queryBox.add(new CQ.Ext.Panel({
            border: false,
            height: 40,
            items: [{
                xtype: 'panel',
                border: false,
                style: "margin:10px 0 0 0",
                layout: {
                    type: 'hbox'
                },
                items: [{
                    xtype: "button",
                    text: "Search",
                    width: 60,
                    tooltip: 'Search',
                    handler: function (button) {
                        var params = cfTab.getParams(cfTab);
                        cfTab.loadStore(params);
                    }
                },{
                    baseCls: "non-existent",
                    html:"<span style='margin-left: 10px;'></span>"
                },{
                    xtype: "button",
                    text: "Clear",
                    width: 60,
                    tooltip: 'Clear the filters',
                    handler: function (button) {
                        $.each(cfTab.fields, function(key, field){
                            field[0].setValue("");
                        });
                    }
                }
                ]
            }]
        }));

        queryBox.setHeight(230);
        queryBox.doLayout();

        var form = cfTab.findByType("form")[0];
        cfTab.fields = CQ.Util.findFormFields(form);
    },

    changeResultsStore: function(){
        var queryBox = CQ.Ext.getCmp(this.PAGES_QUERY_BOX);
        var resultsView = queryBox.ownerCt.findByType("dataview");

        var rvStore = resultsView[0].store;
        rvStore.proxy = new CQ.Ext.data.HttpProxy({
            url: "/bin/mycomponents/objectfinder/pageresults.json",
            method: 'GET'
        });
    }
};

(function(){
    var INTERVAL = setInterval(function(){
        var c = MyComponents.ContentFinder;
        var tabPanel = CQ.Ext.getCmp(CQ.wcm.ContentFinder.TABPANEL_ID);

        if(tabPanel){
            clearInterval(INTERVAL);
            c.addPageFilters();
            c.changeResultsStore();
        }
    }, 250);
})();




1 comment:

  1. Thanks for great contents in this post as well.

    I found GetTemplates servlet returned 500 HTTPS status as there were 3 AEM pre-set nodes having no "jcr:content/sling:resourceType" property.
    To avoid it, availability check should be added like below.

    node = results.nextNode();
    // check if node has property of sling:resourceType or not
    if(node.hasProperty("jcr:content/sling:resourceType")){
    path = node.getProperty("jcr:content/sling:resourceType").getString();
    }
    else{
    // log.error("something to tell") ;
    continue ;
    }

    ReplyDelete