by Kaushal Mall

Created

August 27, 2012

This is something which should come very handy at every CQ implementation. The requirement for vanity URLs is that you cant have two pages in CQ with the same vanity URL. In this blog I will try to go over an implementation which will prevent your content authors from entering duplicate vanity URL in the first place.

1. First thing you will need to do is override the OOTB page component dialog and the tab_basic node. To do this, please copy the following nodes

    • /libs/foundation/components/page/dialog
    • /libs/foundation/components/page/tab_basic

2. Paste these nodes as the child of your projects page component whose “sling:resourceSuperType” is “foundation/components/page”

 

3. For the dialog node, change the path to the basic tab to match the path of the “tab_basic” you pasted in your projects page component.
4. For the “tab_basic” node update the “tab_basic/items/vanity/items/vanityPath/fieldConfig” node to add the following two properties
  • vtype
  • vtypeText
5. Override  the following in your apps folder
  • /libs/cq/ui/widgets/js.txt
  • /libs/cq/ui/widgets/source/widgets
6. To your overridden widgets directory at “/apps/cq/ui/widgets/source/widgets”, add a file called duplicateVanityCheck.js
CQ.Ext.apply(CQ.Ext.form.VTypes, {
duplicateVanityCheck: function(v, f) {var dialog = f.findParentByType("dialog");
var dialogPath = dialog.path;
var cqresponse = CQ.HTTP.get("/apps/duplicateVanityCheck?vanityPath="+v+"&pagePath="+dialogPath);
 
var json = eval(cqresponse);
var vanitypathsjson = json.responseText;
var JSONObj = JSON.parse(vanitypathsjson);
var jsonVanityPath = JSONObj.vanitypaths;
 
if (jsonVanityPath.length == 0) {
return true;
} else {
// check whether the path of the page where the vanity path is defined matches the dialog's path
// which means that the vanity path is legal
return false;
}
 
//alert( "Checking Duplicate" );
}
});

 

7. In your overridden js.txt file, add this line

  • widgets/duplicateVanityCheck.js

 

8. You also need to make sure that your foundation client lib has a dependency on “cq.widgets”.

9. Now, we have to create an OSGi bundle that will query the JCR for an entered vanity and return the JSON that will be used by duplicateVanityCheck.js. The code for this class will look something like below

/**
 * @scr.component immediate="true" metatype="false"
 * @scr.service interface="javax.servlet.Servlet"
 * @scr.property name="sling.servlet.methods" values.0="GET"
 * @scr.property name="sling.servlet.paths" values.1="/apps/duplicateVanityCheck"
 */
 
public class VanityDuplicateCheck extends SlingAllMethodsServlet{
 
    private static final Logger logger = LoggerFactory.getLogger(VanityDuplicateCheck.class);
 
    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
 
        try{
            Session session = request.getResourceResolver().adaptTo(Session.class);
            final String vanityPath = request.getParameter("vanityPath");
            final String pagePath = request.getParameter("pagePath");
            logger.info("vanity path parameter passed is {}", vanityPath);
            logger.info("page path parameter passed is {}", pagePath);
            try {
                QueryManager qm = session.getWorkspace().getQueryManager();
                String xpath = "//element(*)[sling:vanityPath='"+ vanityPath + "']";
                logger.info("xpath is {}", xpath);
 
                Query query = qm.createQuery(xpath, Query.XPATH);
                logger.info("Xpath Query Statement is {}", query.getStatement());
                QueryResult result = query.execute();
                NodeIterator nodes = result.getNodes();
                logger.info("result is ", result.getNodes().toString());
 
                TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());
 
                tidyJSONWriter.object();
 
                tidyJSONWriter.key("vanitypaths").array();
 
                response.setContentType("text/html");
 
                while (nodes.hasNext()) {
                    Node node = nodes.nextNode();
                    logger.info("Node path is {}", node.getPath());
                    logger.info("Page path is {}", pagePath);
                    if(node != null && node.getPath().contains("/content"))
                    {
                        // check whether the path of the page where the vanity path is defined matches the dialog's path
                        // which means that the vanity path is legal.
                        if(node.getPath().equals(pagePath))
                        {
                            //do not add that to the list
                            logger.info("Node path is {}", node.getPath());
                            logger.info("Page path is {}", pagePath);
                        } else {
                            tidyJSONWriter.value(node.getPath());
                        }
                    }
                }
 
                tidyJSONWriter.endArray();
                tidyJSONWriter.endObject();
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
            }
            catch(RepositoryException re){
                logger.error( "Error in doGet", re );
            }
        } catch (JSONException e) {
            logger.error( "Error in doGet", e );
        }
    }

8. At this point you should have everything you need to prevent content authors from entering duplicate vanity URL’s. If they do enter a URL that is already in use, it will fail the validation and they will see an error message similar to what you entered in the vtypeText property.

 

Enjoy..and as always, please leave comments/questions and I will try to answer them as soon as possible.

COMMENTS

  • By Tom - 8:44 AM on November 29, 2012  

    Is there any way you can add an rss feed or email feed to your blogs? I’d like to get notifications vs. checking your blog intermittently.

  • By Vinoth - 1:45 AM on January 4, 2013  

    Hi,

    I am very new to CQ5 facing issue with below two things,

    1. I don’t understand point no “8. You also need to make sure that your foundation client lib has a dependency on “cq.widgets”.

    2. On creating OSGI bundle and importing required packages i am getting below error, searched for solution in google but could not able to figure out the issue, it would be great if you can provide an solution, find below my Java file

    a. Query.XPATH is undefined
    b. hasNext() is undefined for nodes.hasNext()
    c. TypeMismatch cannot convert from int to Node

    /**
    * @scr.component immediate=”true” metatype=”false”
    * @scr.service interface=”javax.servlet.Servlet”
    * @scr.property name=”sling.servlet.methods” values.0=”GET”
    * @scr.property name=”sling.servlet.paths” values.1=”/apps/duplicateVanityCheck”
    */

    package com.mycompany.test;

    import java.io.IOException;

    import javax.jcr.Node;
    import javax.jcr.RepositoryException;
    import javax.jcr.Session;
    import javax.jcr.query.QueryManager;
    import javax.jcr.query.QueryResult;
    import javax.servlet.ServletException;

    import it.tidalwave.imageio.util.Logger;

    import org.apache.sling.api.SlingHttpServletRequest;
    import org.apache.sling.api.SlingHttpServletResponse;
    import org.apache.sling.api.servlets.SlingAllMethodsServlet;
    import org.apache.sling.commons.json.JSONException;
    import org.slf4j.LoggerFactory;

    import com.day.cq.commons.TidyJSONWriter;
    import com.day.cq.search.Query;
    import com.sun.org.apache.xalan.internal.xsltc.NodeIterator;

    public class VanityDuplicateCheck extends SlingAllMethodsServlet{

    private static final Logger logger = (Logger) LoggerFactory.getLogger(VanityDuplicateCheck.class);

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {

    try{
    Session session = request.getResourceResolver().adaptTo(Session.class);
    final String vanityPath = request.getParameter(“vanityPath”);
    final String pagePath = request.getParameter(“pagePath”);
    logger.info(“vanity path parameter passed is {}”, vanityPath);
    logger.info(“page path parameter passed is {}”, pagePath);
    try {
    QueryManager qm = session.getWorkspace().getQueryManager();
    String xpath = “//element(*)[sling:vanityPath='”+ vanityPath + “‘]”;
    logger.info(“xpath is {}”, xpath);

    Query query = qm.createQuery(xpath, Query.XPATH);
    logger.info(“Xpath Query Statement is {}”, ((javax.jcr.query.Query) query).getStatement());
    QueryResult result = ((javax.jcr.query.Query) query).execute();
    NodeIterator nodes = (NodeIterator) result.getNodes();
    logger.info(“result is “, result.getNodes().toString());

    TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());

    tidyJSONWriter.object();

    tidyJSONWriter.key(“vanitypaths”).array();

    response.setContentType(“text/html”);

    while (nodes.hasNext()) {
    Node node = nodes.next();
    logger.info(“Node path is {}”, node.getPath());
    logger.info(“Page path is {}”, pagePath);
    if(node != null && node.getPath().contains(“/content”))
    {
    // check whether the path of the page where the vanity path is defined matches the dialog’s path
    // which means that the vanity path is legal.
    if(node.getPath().equals(pagePath))
    {
    //do not add that to the list
    logger.info(“Node path is {}”, node.getPath());
    logger.info(“Page path is {}”, pagePath);
    } else {
    tidyJSONWriter.value(node.getPath());
    }
    }
    }

    tidyJSONWriter.endArray();
    tidyJSONWriter.endObject();
    response.setContentType(“application/json”);
    response.setCharacterEncoding(“UTF-8″);
    }
    catch(RepositoryException re){
    ((org.slf4j.Logger) logger).error( “Error in doGet”, re );
    }
    } catch (JSONException e) {
    ((org.slf4j.Logger) logger).error( “Error in doGet”, e );
    }
    }
    }

  • By David - 9:47 AM on January 4, 2013  

    Hi, Kaushal. I’m not certain I understand what I need to do in step #8 – can you explain this further? Thanks!

  • By AskingWhy - 3:28 AM on March 18, 2013  

    Nice, but why doesn’t CQ provide this out of the box? Practically every site that uses vanity URLs needs this, because with duplicates one will always not work – the first match in the jcrresolver always wins.