How to prevent users from entering duplicate vanity URL’s

Posted on Monday, August 27, 2012 By

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.

5:49 PM Permalink

CQ Selector driven results page

Posted on Sunday, August 19, 2012 By

In this article I would like to talk about CQ pages where content is based on the Sling selectors. Recently I have worked on a simple requirement for a customer project where a page would have a drop-down (or multiple) menu to filter certain types of content. In the demo below I have a simple set of Adobe products where I want to be able to filter by the product name and/or the product version. Let’s check out the demo first (focus on the selection and the URL changes):

Selector driven content demo

As you can see from the quick demo. Dropdown selection would trigger a URL redirection where content is filtered based on the sling selector. For example:

products.illustrator.all.html displays only Adobe Illustrator products, all versions.
products.illustrator.cs4.html displays only the Adobe Illustrator CS4 product.
products.photoshop.cs6.html displays only the Adobe Photoshop CS6 product.
…so on and so on…

And for demo purposes, we have created pages with a Product component with the following fields:

Now let’s get to see how the filtering works. Here I will provide code pertaining to the sling selector processing and query only.

Sling selector processing (Pagination also factored in):

    // page number should be request param not selector
    int pageNum = 0;
    if (request.getParameter("page") != null) {
        try{
            pageNum = new Integer(request.getParameter("page")).intValue();
        } catch(NumberFormatException e){}
    }
    if(pageNum  0){
        // if product and version present
        if (selectors.length == 2) {
            selectedProduct = selectors[0].trim();
            selectedVersion = selectors[1].trim();
        } else if (selectors.length == 1) {
            if (selectors[0].trim().equalsIgnoreCase("cs4")) selectedVersion = "cs4";    
            else if (selectors[0].trim().equalsIgnoreCase("cs5")) selectedVersion = "cs5";    
            else if (selectors[0].trim().equalsIgnoreCase("cs6")) selectedVersion = "cs6";     
            else if (productsMap.containsKey((String) selectors[0].trim())) {
                selectedProduct = selectors[0].trim();
            } else {
                selectedProduct = "all";
                selectedVersion = "all";
            }
        } else {
            selectedProduct = "all";
            selectedVersion = "all";
        }
    }

The above code would determine the product and the version selected by the user (since they are passed in the URL). And once we have the product, version determined, we can run the following query and display the results in tabular format just like in the demo:

 

    QueryBuilder builder = null;
    Map<String, Object> map = null;
    Query query = null;
    SearchResult result = null;
 
    map = new HashMap<String, Object>();
    map.put("path", "/content/demo/en/products");
    map.put("type","cq:Page");
 
    map.put("1_property","jcr:content/cq:template");
    map.put("1_property.value","/apps/demo/templates/productspage"); // query only productspage
    map.put("2_property","jcr:content/par_main/product/sling:resourceType"); // query for pages with product component
    map.put("2_property.value","demo/components/content/product");
 
    if (!selectedProduct.equals("all") && !selectedVersion.equals("all")) {
        map.put("3_property","jcr:content/par_main/product/category");
        map.put("3_property.value",selectedProduct);
        map.put("4_property","jcr:content/par_main/webinar/productVersion");
        map.put("4_property.value",selectedVersion);
 
    } else if (!selectedProduct.equals("all")) {
        map.put("3_property","jcr:content/par_main/product/category");
        map.put("3_property.value",selectedProduct);
 
    } else if (!selectedVersion.equals("all")) {
        map.put("3_property","jcr:content/par_main/product/productVersion");
        map.put("3_property.value",selectedVersion);
    }
    map.put("5_orderby","@jcr:content/par_main/product/category");
    map.put("5_orderby.sort","asc");
    map.put("6_orderby","@jcr:content/jcr:title");
    map.put("6_orderby.sort","asc");   
 
    builder = resource.getResourceResolver().adaptTo(QueryBuilder.class);
    query = builder.createQuery(PredicateGroup.create(map), currentNode.getSession());
    query.setStart(startIndex);
    query.setHitsPerPage(15);
    result = query.getResult();

And the above queries for pages that are using the product template, with a product component in it, and filter by either product category or the version if they are passed in.

Enjoy!

10:32 PM Permalink

How to integrate with other web services without creating OSGi packages

Posted on Sunday, August 19, 2012 By

A lot of enterprise grade systems require some sort of integration with other services these days, to extend and bring in more features into the systems. In this blog post I would like to show a quick and easy way to integrate CQ5 with other web services. Examples below will be showing a RESTful webservice but other webservices like SOAP will take similar approach.

Webservices integration usually involves building a integration layer. A Java Servlet would definitely be a good candidate. But to avoid building / compiling / packaging / deploying such code into an OSGi container, one can easily write a JSP (on the fly) in CQ to handle the integration.

Create an integration layer page:

This integration page is basically to create an instance of a CQ Page so that the rest of the content pages can communicate to it (Things under /app are not exposed or cannot be called directly). And extra benefit about this CQ Page -> You can control who has access to it!

Note the above page is using “Demo – API Page Template”, and here is the mapping:

 

And apipage.jsp is nothing more than what you want to surface in a typical Servlet.  Here is snippet of the code:

if (request.getParameter("action") != null) {
    if (request.getParameter("action").equals("getSomething")) {
        Node demoNode = resourceResolver.getResource(INTEGRATION_DESIGN_NODEPATH).adaptTo(Node.class);
        response.setContentType("application/json");
        out.write(getSomething(demoNode));
 
    } else if (request.getParameter("action").equals("getCategories")) {
        Node demoNode = resourceResolver.getResource(INTEGRATION_DESIGN_NODEPATH).adaptTo(Node.class);
        response.setContentType("application/json");
        out.write(getCategories(demoNode));
 
    } else if (request.getParameter("action").equals("getFields") &amp;&amp;
            request.getParameter("param1") != null &amp;&amp;
            request.getParameter("param2") != null) {
        response.setContentType("application/json");
        out.write(getFields(
            request.getParameter("param1"),
            request.getParameter("param2"));
 
    } else {
        response.setContentType("application/json");
        out.write("{\"error\":\"error\"}");
    }
}

 

And derived from the above, we now have the following links to retrieve data:

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getSomething

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getCategories

http://_server_:_port_/content/integrationdemo/en/integration/integrationapi?action=getFields&param1=test&param2=data

 

The integration implementation will be per your requirements, but at a high level you can pretty much do everything-in-a-servlet inside a JSP. JSPs are Servlets after all. Here is a snippet of a http call (to a webservice) inside the same JSP:

 

static String getFields(String apiKey, String accessToken, String someJson) {
    HttpClient httpClient = new HttpClient();
    HttpClientParams httpClientParams = new HttpClientParams();
    DefaultHttpMethodRetryHandler defaultHttpMethodRetryHandler = new DefaultHttpMethodRetryHandler(0, false);
    httpClientParams.setParameter(HttpClientParams.RETRY_HANDLER, defaultHttpMethodRetryHandler);
    httpClient.setParams(httpClientParams);
 
    PostMethod post = new PostMethod(EXTERNAL_WEBSERVICE_API_URL+apiKey.trim()+"/URL_EXAMPLE/test?oauth_token="+accessToken.trim());
    post.addRequestHeader("Accept", "application/json");
    post.addRequestHeader("Content-Type", "application/json");
    post.setRequestBody(someJson);
 
    try {
        httpClient.executeMethod(post);
        String result = post.getResponseBodyAsString();
 
        if (result != null &amp;&amp; !result.equals("")) {
            JSONObject returnObj = new JSONObject(result);
            return returnObj.toString();
        } else {
            return "";
        }
    } catch (Exception ex) {
        return ex.getMessage();
    }   
}

 

And finally, as an option (if you want to integration the CQ authoring experience with an external webservice), you can expose the data retrieved inside a CQ Component dialog field, you will need to configure the widget to retrieve webservice data. Details are documented in:

http://dev.day.com/docs/en/cq/current/widgets-api/index.html?class=CQ.form.Selection

Here’s a quick example:

And the result:

 

Enjoy~

11:21 AM Permalink

How to fix CRXDE performance issues

Posted on Wednesday, June 6, 2012 By

Is CRXDE too slow or is not loading at all? Try these steps below to possibly fix the issue.

Make sure the crxde:paths property doesn’t include anything that you dont want to load in CRXDE.

  • Open the Content Explorer and browse to /etc/crxde/profiles/default, on the right hand panel there will be the crxde:paths property, this defines what nodes CRXDE will try to load. Having nodes like /content will make it slow/unresponsive.

Locate and open the CRXDE.ini file and assign more memory to CRXDE.

  • On OSX right click on CRXDE.app and select Show Package Contents.
  • Browse to Contents/MacOS
  • Open the CRXDE.ini file and change the values for Xms, Xmx and the MaxPermSize to suit your needs and what your system will support.

Delete the .crxde folder

  • CRXDE created a hidden .crxde folder under the users home directory.
  • Delete that folder

Start CRXDE from command line.

  • Open -a CRXDE –args -clean (This is for OSX only)

Hope this helps. Please leave a comment/question and I will try to answer them as soon as I can.

11:26 AM Permalink

How to track CQ component clicks in SiteCatalyst

Posted on Saturday, May 19, 2012 By

When SiteCatalyst’s tracking is enabled on a site, the overall site traffic and page-to-page traffic tracking is pretty much given and administrators usually do not have to do anything. But one of the requirements that most companies desire is the component tracking capability – to be able to tell which component on a page the users are coming from. Here’s a typical scenario:

  • A featured article is displayed inside a Slideshow or teaser type component on a page, e.g. http://www.example.com/home.html.
  • The same featured article is displayed inside another component (e.g. Spotlight component) on the same page – http://www.example.com/home.html

Requirement:

  • To be able to track traffic at component level.

In order to do this, two SiteCatalyst variables will need to be introduced:

  • A variable on tracking what components are “clicked from”. In this example I named the variable “Clicked From Component”.
  • A variable on tracking what pages the components lead to, or “clicked to”. In this example I named the variable “Clicked To Page”.

And it is critical to set up a correlation between these two variables:

On to CQ:

On the CQ side, components will need to be customized to pass the variables onto the “clicked to” page so traffic can be tracked. For instance, links can be modified to have additional parameters like the following:

http://server:port/news/2012/5/component-tracking-example.html?trackFrom=Spotlight

And on pages that should be tracked, the following code can be inserted to surface the variables inside CQ’s clickstreamcloud:

if (request.getParameter("trackFrom") != null) {
        String trackFromComponent = (String) request.getParameter("trackFrom");
        %>
        <span record="'trackComponent',{'clickedFrom':'<%=trackFromComponent%>','clickedTo':'<%=request.getRequestURL()%>'}"></span>
        <%
}

 

Then these parameters will be surfaced inside CQ’s clickstreamcloud:

To map these events to SiteCatalyst variables, (after traffic variables setup in SiteCatalyst), simply Edit the clickstreamcloud and map the following events to variables:

  • clickedFrom – “Clicked From Component”
  • clickedTo – “Clicked To Page”

 

 

Once all the above is done, you may click around and start viewing the “Clicked From Component” report in SiteCatalyst. And to view how components are “clicked to” certain pages, the correlation report surface that very nicely.

Clicked From Component Report

 

Correlation Report

2:21 PM Permalink