AEM - Source Code, Code Reviews, Build and Release Management

Goal


Every other's SCM is icky. So you may find the following process lengthy, confusing and lead to the decision, let's continue with the way it is - and that is fine given the nature of project.

Here is a process (not fork based) on setting up the project source code GIT repo in Bit Bucket (free for 5 users), Source Tree GIT Client, Git Flow for Branch Management and creating Releases, Hotfixes, in a collaborative environment. Configuring Jenkins for Continuous Integration is discussed here

The process discussed below is based on GIT Flow and suggests creating 5 type of branches

                         Development branch -  develop
                         Production branch - master
                         Feature branches - prefixed with feature/
                         Release branches - prefixed with release/
                         Hotfix branches - prefixed with hotfix/

Solution


Create Repository in BitBucket

1) Sign up for creating a Repository. The admin, for example, experience.aem@gmail.com (username: eaem) signs up and creates a repo by clicking Create -> Create repository

2) Enter project name - experience-aem-intranet and other necessary details





Clone Repository

1) Create a local clone by accessing https://bitbucket.org/eaem/experience-aem-intranet/overview clicking on Clone in Source Tree




2) In Source Tree, enter local directory path, click Clone



3) Master branch clone gets created on the file system and bookmarked in source tree



4) If you are using multiple bitbucket accounts in source tree, add the credentials in experience-aem-intranet repo settings




Initialize Repo with GITFlow

1) Create an empty file, eg. readme.txt in local repo - C:\dev\code\projects\cq6-extensions\experience-aem-intranet\readme.txt; commit and push, to create local & remote (origin) master branch



2) Using GIT Flow gives a neat, standardized approach; streamlines the entire release process and works well in collaborative environments. The next step is to initialize repo experience-aem-intranet with GIT Flow



      Branch develop gets created



      Push the created masterdevelop branches to origin




3) The significance of master branch is, it contains released/deployed codebase (the one installed on production). develop branch contains feature code, bug fixes. Quality Assurance team should generally get their deployments built out of develop branch, for feature testing.

So its good to have some write access control on master and develop branches to avoid accidental checkins, make it a policy to review any code ready for QA or Production. In this example, the project admin eaem acts as reviewer. The job of a reviewer is to make sure feature/bugfix code submitted for review is clean, conventions followed, proper commenting added, any best practices internally are followed etc...

4) For code review process, set access restrictions on master and develop branches by accessing bitbucket https://bitbucket.org/eaem/experience-aem-intranet/admin



5) This being a private repository, only team members should be allowed to access source code for feature development. For this example, lets add user nalabotu - https://bitbucket.org/eaem/experience-aem-intranet/admin/access




Create New AEM Bundle

1) Admin of experience-aem-intranet eaem creates new module intranet-portal (remove line breaks in the below command) using archetype https://github.com/Adobe-Marketing-Cloud/aem-project-archetype

C:\dev\code\projects\cq6-extensions\experience-aem-intranet>"C:\Program Files\Java\jdk1.7.0_25\bin\java" -Dmaven.home=C:\dev\code\install\apache-maven-3.1.0 -Dclassworlds.conf=C:\dev\code\install\apache-maven-3.1.0\bin\m2.conf -Dfile.encoding=UTF-8 -classpath C:\dev\code\install\apache-maven-3.1.0\boot\plexus-classworlds-2.4.2.jar org.codehaus.classworlds.Launcher --fail-fast --lax-checksums -DinteractiveMode=false 
-DgroupId=com.experienceaem.intranet 
-DartifactId=intranet-portal 
-Dversion=1.0-SNAPSHOT 
-DarchetypeGroupId=com.adobe.granite.archetypes 
-DarchetypeArtifactId=aem-project-archetype 
-DarchetypeVersion=10 
-Dpackage=com.experienceaem 
-DappsFolderName=experienceaem-intranet 
"-DartifactName=Experience AEM Intranet Portal" 
"-DcomponentGroupName=Experience AEM" 
-DcontentFolderName=experience-aem-intranet 
-DcssId=experience-aem 
"-DpackageGroup=Experience AEM" 
"-DsiteName=Experience AEM Intranet" org.apache.maven.plugins:maven-archetype-plugin:RELEASE:generate


2) The project module experience-aem-intranet\intranet-portal in IDE



3) Install module package using mvn -PautoInstallPackage clean install and a sample page http://localhost:4502/content/experience-aem-intranet/en.html should become available



4)  Add target (folder name) to C:\dev\code\projects\cq6-extensions\experience-aem-intranet\.gitignore, as folders generated via build are not needed in repository

5) The current branch is develop (if it isn't double click to switch to develop ), commit and push the code to origin develop branch



6) At this point the module Intranet Portal is available for feature development


Feature Development (Bug Fixing)

1) Say a new feature story is assigned to user nalabotu (developing on MAC). User starts development by creating a feature branch feature/EAEM-users-json-servlet-sreek  (<Project-Name>-<Brief-Description>-<Username>)





2) Code the servlet com.experienceaem.core.servlets.GetUsersJSON, test it by accessing http://localhost:4502/bin/experience-aem/users.json, commit and push from local feature branch to origin (select create pull request)



   Before creating the pull request make sure you pull changes from remote develop and merge the latest develop changes into feature branch. It's a good practice to merge latest code into feature branches daily or should do it atleast before creating a pull to develop request, test and make sure feature changes are working ok, with latest develop code

3) The remote branch should have been created - feature/EAEM-users-json-servlet-sreek

4) In Step 2, user has selected Create pull request while committing; a browser tab with create pull request opens, make sure the feature to be merged is on left and destination develop on right, select the reviewer eaem, click Create pull request (pull request gets created and email sent to reviewers)



5) In regular GIT Flow, developer clicks Finish feature in source tree and the feature branch gets merged to local develop, which can then be pushed to origin develop. For review process, admin eaem has push restrictions enabled on develop and master, so Finish feature is not going to work here, as the developer nalabotu with no write access to remote develop, cannot push the local develop changes. Doing so will result in following error; so when the developer is done with coding a feature, he/she can simply delete the branch (instead of clicking Finish feature), after creating the pull request and it gets merged to develop branch by reviewer.






Review Process - admin eaem

1) User eaem, clicks on the review link in email notification, adds a comment (developer nalabotu is notified by email)



Incorporate Review Comments - dev nalabotu

1) Developer implements review comments, commits, pushes & creates a pull request as explained above; the pull request created earlier gets updated with latest changes





Reviewer merges the changes - eaem

1) Reviewer eaem is happy with the implementation, clicks Merge to pull in the feature changes into develop. At this point the feature is ready for testing by quality team



2) Hooks can be configured in bitbucket on branches (develop, master etc.) to kickoff build and deploy packages to test CQ servers, when any changes are pulled into develop branch (For Continuous Integration check this post)


Creating Release

1) When feature development/testing is done and code is all set to move to production, the project admin (or any user with necessary permissions on develop, master and release branches) uses GIT Flow -> Create release to create a release, say v1.1.0 (based on develop)





2) Make the necessary version changes in pom.xml, 1.1.0 (following the <Major>.<Minor>.<Patch> convention) test on local CQ, commit and push the changes to origin. Some use even/odd convention, even number in minor for release and odd number for development, but for pre-release or development, -SNAPSHOT can be used in version; so 1.2.3-snapshot would be a lower version number than 1.2.3, for more information check this post)



3) The release branch release/v1.1.0, created in origin (remote)...



4) Click Finish Release to create the release and tag it with some message





5) The release changes (versions in pom.xml) are merged to master and develop branches. Push to origin...




6) Switch to master branch, build and do some smoke testing on local CQ. Upload the deployment package to a centralized location for Production deployment. Develop branch has latest code and Master branch has code deployed to Production. 


Prepare Develop for Next Iteration

1) Release 1.1.0 was created, code was promoted to master, its time to prepare develop for next set of features

2) Project admin eaem, switches to develop branch and makes changes to version number in pom.xml , 1.2.0-SNAPSHOT (So the next release is 1.2.0). Commit and Push to Origin



3) Send an email to developers to merge the latest develop changes into their feature branches to update them with latest version numbers


Production Bugs HotFixes

1) If you need to patch the latest release without picking up new features from the development branch, you can create a hotfix branch from the latest deployed code in master. Once you’ve made your changes, the hotfix branch is then merged back into both the master branch (to update the released version) and the development branch (to make sure the fixes go into the next release too)

2) Say the servlet GetUsersJSON coded by developer nalabotu, has a bug. It was supposed to return userids and not user node paths; the bug was assigned to developer(admin) eaem.

3) Project admin eaem setups a hotfix branch by using GIT Flow -> Start New HotFix



4) Say every end of month, bug fixes are promoted to production via hot fix branch. Name the hotfix may-bug-fixes. Hotfix branches are always based on master (production code)



5) Developers working on production bugfixes do not create feature branches. Fetch hotfix branch and bug fixes from all developers are pushed to one hotfix branch, here it's may-bug-fixes. Quality team creates a build out of hotfix branch, verifies fixes for sign-off.

6) Developer eaem fixes the servlet and pushes code to origin hotfix branch (similarly over the next few days, developers working on production bugs, sync hotfix branch, fix bugs and push the code to remote hotfix branch)



7) When the fixes are tested and ready to move to production, project admin, here eaem, or any developer having write permissions on master and develop branches, finishes the hotfix by clicking Git Flow -> Finish Hotfix





8) The hotfix branch gets merged back into master and develop branches. Push the changes, if there are any merge conflicts (say the same lines of code were modified in develop as well), resolve the conflicts...



9) Switch to master, generate the deployment packages, smoke test and upload the packages to a centralized location for deployment team

10) When the hotfix is finished, the bug fixes for production are automatically pulled (merged) into develop branch and available in next feature releases as well.



AEM 6 SP2 - Classic UI Restrict Moving Folder with N level Subfolders in Damadmin

Goal


Restrict moving folders with deep nesting and thousands of assets, triggering resource intensive processing in CRX

Demo | Package Install


Error when Subfolder Levels > 1 





Move in Tree Not Allowed




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-restrict-folder-move

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-restrict-folder-move/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classicui-restrict-folder-move/clientlib/js.txt, add the following

                         restrict-move.js

4) Create file ( type nt:file ) /apps/classicui-restrict-folder-move/clientlib/restrict-move.js, add the following code

(function () {
    if (window.location.pathname !== "/damadmin") {
        return;
    }

    var NESTING_ALLOWED = 1, TREE = "cq-damadmin-tree";

    /*//the original move dialog fn
    var cqMoveDialog = CQ.wcm.Page.getMovePageDialog;

    //override ootb function
    CQ.wcm.Page.getMovePageDialog = function (path, isPage) {
        var dialog = cqMoveDialog(path, isPage);

        function handler(isNotAllowed) {
            if (isNotAllowed) {
                CQ.Ext.Msg.alert("Error", "Moving Folder with > " + NESTING_ALLOWED + " level subfolders, not allowed");
                dialog.close();
            }
        }

        isMoveAllowed(path).then(handler);

        return dialog;
    };*/

    //the original move dialog fn
    var cqMovePage = CQ.wcm.SiteAdmin.movePage;

    //override ootb function
    CQ.wcm.SiteAdmin.movePage = function () {
        var selections = this.getSelectedPages();

        if(selections.length == 0){
            return;
        }

        var that = this;

        function handler(isNotAllowed) {
            if (isNotAllowed) {
                CQ.Ext.Msg.alert("Error", "Moving Folder with > " + NESTING_ALLOWED + " level subfolders, not allowed");
                return;
            }

            cqMovePage.call(that);
        }

        isMoveAllowed(selections[0].id).then(handler);
    };

    var INTERVAL = setInterval(function(){
        var tree = CQ.Ext.getCmp(TREE);

        if(tree){
            clearInterval(INTERVAL);
            handleTreeNodeMove(tree);
        }
    }, 250);

    function getFolderJson(path) {
        return $.ajax( path + "." + (NESTING_ALLOWED + 1) + ".json" );
    }

    function isMoveAllowed(path) {
        function handler(data) {
            return reachedMaxNestedLevel(data, NESTING_ALLOWED);
        }

        function reachedMaxNestedLevel(folder, nestingNum) {
            for (var x in folder) {
                if (!folder.hasOwnProperty(x) || !isFolder(folder[x])) {
                    continue;
                }

                if (nestingNum == 0) {
                    return true;
                }

                if (reachedMaxNestedLevel(folder[x], nestingNum - 1)) {
                    return true;
                }
            }

            return false;
        }

        return getFolderJson(path).then(handler);
    }

    function handleTreeNodeMove(tree) {
        var listeners = tree.initialConfig.listeners;

        tree.removeListener("beforenodedrop", listeners.beforenodedrop, tree);

        tree.on("beforenodedrop", function (dropEvent) {
            CQ.Ext.Msg.alert("Error", "Moving tree nodes not allowed, use Move... in grid");
            return false;
        });
    }

    function isFolder(node) {
        return node["jcr:primaryType"] == "sling:OrderedFolder";
    }
}());


AEM 6 SP2 - Classic UI Task Management Sort Task Projects Tree Nodes

Goal


Alphabetically sort the Task Projects tree in Task Management console http://localhost:4502/libs/cq/taskmanagement/content/taskmanager.html#/tasks

Demo | Package Install


Product



Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-sort-task-projects

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-sort-task-projects/clientlib and set a property categories of String type to cq.taskmanagement

3) Create file ( type nt:file ) /apps/classicui-sort-task-projects/clientlib/js.txt, add the following

                         sort.js

4) Create file ( type nt:file ) /apps/classicui-sort-task-projects/clientlib/sort.js, add the following code

(function(){
    var pathName = window.location.pathname;

    if( pathName.indexOf("/libs/cq/taskmanagement/content/taskmanager.htm") != 0 ){
        return;
    }

    var TREE_ID = "cq-taskmanager-tree";

    function sort(treePanel, asc){
        treePanel.on('load', function(node){
            node.childNodes.sort(function(a,b){
                a = a["text"].toLowerCase();
                b = b["text"].toLowerCase();
                return asc ? ( a > b ? 1 : (a < b ? -1 : 0) ) : ( a > b ? -1 : (a < b ? 1 : 0) ) ;
            });
        })
    }

    var INTERVAL = setInterval(function(){
        var tree = CQ.Ext.getCmp(TREE_ID);

        if(tree){
            clearInterval(INTERVAL);
            sort(tree, true);
        }
    }, 250);
}());


AEM 6 SP2 - Query Builder Predicate Evaluator for ordering results Ignoring Case

Goal


Create a predicate evaluator for ordering Query Builder - /bin/querybuilder.json results based on node properties, Case Ignored...

Thanks to this blog post for providing insight on predicate evaluators and documentation on docs.adobe.com


So with the following query

                     http://localhost:4502/bin/querybuilder.json?p.limit=20&p.offset=0&eaem-ignore-case.property=jcr:content/metadata/uaDIO:reportName&orderby=eaem-ignore-case&fulltext=*.indd&p.hits=full&p.nodedepth=2&path=/content/dam/Product/Assortments&type=dam:Asset


prettified....

                     p.limit:  20
                     p.offset:  0
                     eaem-ignore-case.property:  jcr:content/metadata/uaDIO:reportName
                     orderby:  eaem-ignore-case
                     fulltext:  *.indd
                     p.hits:  full
                     p.nodedepth:  2
                     path:  /content/dam/Product/Assortments
                     type:  dam:Asset

eaem-ignore-case predicate evaluator getOrderByProperties() returns the property jcr:content/metadata/uaDIO:reportName adding necessary xpath function fn:upper-case and the final xpath query built would be....

                     /jcr:root/content/dam/Product/Assortments//element(*, dam:Asset)[jcr:contains(., '*.indd')] order by fn:upper-case(jcr:content/metadata/@uaDIO:reportName)


Solution


1) Create  a OSGI bundle with case insensitive predicate evaluator class, add the following code

package apps.experienceaem.pe;

import com.day.cq.search.Predicate;
import com.day.cq.search.eval.AbstractPredicateEvaluator;
import com.day.cq.search.eval.EvaluationContext;
import org.apache.felix.scr.annotations.Component;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.query.Row;
import java.util.*;

@Component(metatype = false, factory = "com.day.cq.search.eval.PredicateEvaluator/eaem-ignore-case")
public class CaseInsensitiveOrderByPredicate extends AbstractPredicateEvaluator {
    private static final Logger logger = LoggerFactory.getLogger(CaseInsensitiveOrderByPredicate.class);

    public static final String PROPERTY = "property";

    public String[] getOrderByProperties(Predicate p, EvaluationContext context) {
        Map<String, String> paramMap = p.getParameters();
        List<String> orderProps = new ArrayList<String>();

        for(String param : paramMap.values()){
            orderProps.add("fn:upper-case(" + param + ")");
        }

        return orderProps.toArray(new String[0]);
    }

    /**
     * can be used for further ordering, or scenarios where getOrderByProperties() isn't enough
     *
     * @param predicate
     * @param context
     * @return
     */
    /*public Comparator<Row> getOrderByComparator(final Predicate predicate, final EvaluationContext context) {
        return new Comparator<Row>() {
            public int compare(Row r1, Row r2) {
                int ret = 1;

                if ((r1 == null) || (r2 == null) || (predicate.get(PROPERTY) == null)) {
                    return ret;
                }

                try {
                    ValueMap valueMap1 = context.getResource(r1).adaptTo(ValueMap.class);
                    ValueMap valueMap2 = context.getResource(r2).adaptTo(ValueMap.class);

                    String property1 = valueMap1.get(predicate.get(PROPERTY), "");
                    String property2 = valueMap2.get(predicate.get(PROPERTY), "");

                    ret = property1.compareToIgnoreCase(property2);
                } catch (Exception e) {
                    logger.error(e.getMessage());
                }

                return ret;
            }
        };
    }*/
}

2) Check registered evaluators - http://localhost:4502/system/console/services?filter=%28component.factory%3Dcom.day.cq.search.eval.PredicateEvaluator%2F*%29


AEM 6 SP2 - Accessing CRX Remotely using Jcr Remoting Based On Webdav (DavEx)

Goal


To access CRX remotely in a client java program, JCR Remoting (DavEx) or RMI can be used. This post is on using JCR Remoting (aka WebDav remoting, DavEx). For RMI check this post

More documentation:

http://wiki.apache.org/jackrabbit/RemoteAccess

https://docs.adobe.com/content/help/en/experience-manager-65/developing/platform/access-jcr.html

Solution


1) If maven is used, following are the dependencies

 <dependencies>
      <dependency>
            <groupId>javax.jcr</groupId>
            <artifactId>jcr</artifactId>
            <version>2.0</version>
      </dependency>  
      <dependency>
            <groupId>org.apache.jackrabbit</groupId>
            <artifactId>jackrabbit-jcr2dav</artifactId>
            <version>2.4.0</version>
      </dependency> 
      <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.5</version>
      </dependency>
 </dependencies>

2) Without maven, to connect to CRX in a plain java standalone program, the following jars should be added to classpath. These jars can be downloaded from Adobe repo - Nexus

Assuming jars are available in user's .m2 folder, the following paths should be added to classpath

                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr-commons\2.7.0\jackrabbit-jcr-commons-2.7.0.jar
                      C:\Users\nalabotu\.m2\repository\javax\jcr\jcr\2.0\jcr-2.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr2dav\2.4.0\jackrabbit-jcr2dav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr2spi\2.4.0\jackrabbit-jcr2spi-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi\2.4.0\jackrabbit-spi-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi2dav\2.4.0\jackrabbit-spi2dav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\slf4j\slf4j-api\1.7.5\slf4j-api-1.7.5.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi-commons\2.4.0\jackrabbit-spi-commons-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-webdav\2.4.0\jackrabbit-webdav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\commons-httpclient\commons-httpclient\3.1\commons-httpclient-3.1.jar
                      C:\Users\nalabotu\.m2\repository\commons-codec\commons-codec\1.6\commons-codec-1.6.jar
                      C:\Users\nalabotu\.m2\repository\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar
                      C:\Users\nalabotu\.m2\repository\commons-collections\commons-collections\3.2.1\commons-collections-3.2.1.jar

3) Sample standalone program for connecting to CRX using DavEx and execute query, returning templates (type cq:Template)

package apps;

import org.apache.jackrabbit.commons.JcrUtils;

import javax.jcr.*;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;

public class DavExWebDavRemotingTest {
    public static void main(String[] args) throws Exception{
        String REPO = "http://localhost:4502/crx/server";
        String WORKSPACE = "crx.default";

        Repository repository = JcrUtils.getRepository(REPO);

        Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()), WORKSPACE);
        QueryManager qm = session.getWorkspace().getQueryManager();

        String stmt = "select * from cq:Template";
        Query q = qm.createQuery(stmt, Query.SQL);

        NodeIterator results = q.execute().getNodes();
        Node node = null;

        while(results.hasNext()){
            node = (Node)results.next();
            System.out.println(node.getPath());
        }

        session.logout();
    }
}

4) Sample script to generate random folders in AEM

public class CreateFoldersInAEM {
    private static String PARENT_PATH = "/content/dam/sandbox/experience-aem";

    private static int DEPTH = 2;

    public static void main(String[] args) throws Exception{
        String REPO = "http://localhost:4502/crx/server";
        String WORKSPACE = "crx.default";

        Repository repository = JcrUtils.getRepository(REPO);

        Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()), WORKSPACE);

        Node node = session.getNode(PARENT_PATH + "/");

        addNumericFoldersTwoLevelDeep(node, (DEPTH - 1));

        session.save();

        session.logout();
    }

    private static void addNumericFoldersTwoLevelDeep(Node node, int depth) throws Exception{
        if(depth < 0){
            return;
        }

        int maximum = 99999, minimum = 10000, count = 10;

        List<Integer> range = IntStream.range(minimum, maximum).boxed()
                                    .collect(Collectors.toCollection(ArrayList::new));

        Collections.shuffle(range);

        range.subList(0, count).forEach((randomNum) -> {
            try{
                Node subFolder = node.addNode(String.valueOf(randomNum), "sling:Folder");

                addNumericFoldersTwoLevelDeep(subFolder, (depth - 1));

                addAlphaNumericFoldersTwoLevelDeep(subFolder, (depth - 1));
            }catch(Exception e){
                throw new RuntimeException(e);
            }
        });
    }

    private static void addAlphaNumericFoldersTwoLevelDeep(Node node, int depth) throws Exception{
        if(depth < 0){
            return;
        }

        int count = 10;

        while(count -- > 0){
            Node subFolder = node.addNode(UUID.randomUUID().toString(), "sling:Folder");

            addAlphaNumericFoldersTwoLevelDeep(subFolder, (depth - 1));
        }
    }
}


AEM 61 - Touch UI Multiple Root Paths in Tags Picker

Goal


Support multiple root paths in Tags Picker of Touch UI. For Classic UI check this post

This is a picker extension only, searching inline still returns every tag available

For a similar Path Browser Picker extension check this post

Demo |  Package Install


Tags Picker (Product)



Tags Picker with Multiple Root Paths Configuration (Extension)





Tags Picker with Multiple Root Paths (Extension) eaemTagsRootPaths -  /etc/tags/geometrixx-outdoors/activity, /etc/tags/geometrixx-media/entertainment




Solution


1) Login to CRXDE Lite http://localhost:4502/crx/de, create folder /apps/touchui-tags-picker-custom-root-paths

2) Create folder /apps/touchui-tags-picker-custom-root-paths/tags-picker and file /apps/touchui-tags-picker-custom-root-paths/tags-picker/tags-picker.jsp, add the following code. This picker jsp extends ootb tags picker jsp /libs/cq/gui/components/common/tagspicker to set the data source url /apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper.html (ootb its /libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html set in /libs/cq/gui/components/common/tagspicker/render.jsp). Had the pickerSrc attribute been available as configuration param like rootPath, this step would have not been required...

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="org.apache.commons.lang3.ArrayUtils" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<sling:include resourceType="/libs/cq/gui/components/common/tagspicker" />

<%
    Config cfg = cmp.getConfig();
    String[] eaemTagsPaths = cfg.get("eaemTagsRootPaths", String[].class);

    //tags paths not set, continue with ootb functionality
    if(ArrayUtils.isEmpty(eaemTagsPaths)){
        return;
    }
%>

<script type="text/javascript">
    (function(){
        var EAEM_TAGS_PATHS = "eaemtagsrootpaths",
            BROWSER_COLUMN_PATH = "/apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper.html";

        function changeTagsPickerSrc(){
            var $eaemTagsPicker = $("[data-" + EAEM_TAGS_PATHS + "]");

            if($eaemTagsPicker.length == 0){
                return;
            }

            var browserCP = BROWSER_COLUMN_PATH + '<%=StringUtils.join(eaemTagsPaths, ",")%>';

            $eaemTagsPicker.attr("data-picker-src", browserCP);
        }

        changeTagsPickerSrc();
    }());
</script>

3) Create sling:Ordered folder /apps/touchui-tags-picker-custom-root-paths/content and nt:unstructured node /apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper with sling:resourceType /apps/touchui-tags-picker-custom-root-paths/tag-browser-column

4) To add the picker browser column renderer extension, create folder /apps/touchui-tags-picker-custom-root-paths/tag-browser-column and file /apps/touchui-tags-picker-custom-root-paths/tag-browser-column/tag-browser-column.jsp with the following code. It includes /libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html for getting the columns html and later removes unwanted root nodes

<%@ page import="org.apache.sling.api.request.RequestPathInfo" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%!
    String TAG_BROWSER_COLUMN_PATH = "/libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html";
    String TAG_NAV_MARKER = "eaemTagNavMarker";

    private String getParentTagPath(String tagPath) {
        return tagPath.substring(0, tagPath.lastIndexOf("/"));
    }

    private JSONArray getTagPathsJson(String[] tagPaths){
        JSONArray array = new JSONArray();

        for(String tagPath: tagPaths){
            array.put(tagPath);
        }

        return array;
    }
%>

<%
    RequestPathInfo pathInfo = slingRequest.getRequestPathInfo();
    String tagPaths[] = pathInfo.getSuffix().split(",");

    for(String tagPath: tagPaths){
        String includePath = TAG_BROWSER_COLUMN_PATH + getParentTagPath(tagPath);

%>
        <sling:include path="<%=includePath%>"  />
<%
    }
%>
<div id="<%=TAG_NAV_MARKER%>">
</div>

<script type="text/javascript">
    (function(){
        function removeAddnNavsGetColumn($navs){
            $navs.not(":first").remove(); //remove all additional navs
            return $navs.first().children(".coral-ColumnView-column-content").html("");//get the column of first nav
        }

        function addRootTags(){
            var $tagMarker = $("#<%=TAG_NAV_MARKER%>"),
                $navs = $tagMarker.prevAll("nav"),
                tagPaths = <%=getTagPathsJson(tagPaths)%>,
                rootTags = [];

            //find the root tags
            $.each(tagPaths, function(index, tagPath){
                rootTags.push($navs.find("[data-value='" + tagPath + "']"));
            });

            removeAddnNavsGetColumn($navs).append(rootTags);

            //remove the tag marker div
            $tagMarker.remove();
        }

        addRootTags();
    }());
</script>

5) In the component dialog add necessary configuration eaemTagsRootPaths and sling:resourceType /apps/touchui-tags-picker-custom-root-paths/tags-picker. For example, here is a sample configuration added on ootb page properties /libs/foundation/components/page/cq:dialog/content/items/tabs/items/basic/items/column/items/title/items/tags. This is for demonstration only, never touch /libs





AEM 6 SP2 - Handling Custom Protocol in Link Href in Rich Text Editor

Goal


Adding protocols like tel: (or any custom) in anchor tag href attribute, may not be printed as entered in RTE as link checker com.day.cq.rewriter.linkchecker.impl.LinkCheckerImpl and XSS protection com.adobe.granite.xss.impl.HtmlToHtmlContentContext AntiSamy removes unrecognized protocols during component rendering. Here is the warning seen in error log

06.05.2015 10:07:45.213 *INFO* [0:0:0:0:0:0:0:1 [1430924865002] GET /content/geometrixx/en.html HTTP/1.1] com.adobe.granite.xss.impl.HtmlToHtmlContentContext AntiSamy warning: The a tag contained an attribute that we could not process. The href attribute had a value of "tel&#58;18475555555". This value could not be accepted for security reasons. We have chosen to remove this attribute from the tag and leave everything else in place so that we could process the input.

To get around this problem some configuration changes (Package Install) are required in CQ

Thank you Amrit Verma for the tip..

Solution


1) Overlay /libs/cq/xssprotection/config.xml in /apps - /apps/cq/xssprotection/config.xml

2) Add the protocol, say telURL

<regexp name="telURL" value="tel:[0-9]+"/>




3)  Add telURL configuration to the accepted list of anchor href

  <attribute name="href">
   <regexp-list>
    <regexp name="onsiteURL"/>
    <regexp name="offsiteURL"/>
    <regexp name="telURL"/>
   </regexp-list>
  </attribute>




4) Add tel: to the Link Checker Special Link Prefixes http://localhost:4502/system/console/configMgr/com.day.cq.rewriter.linkchecker.impl.LinkCheckerImpl




5) With these configuration changes any tel: links in RTE should render fine...