Article summary

Summary

This AEM development community article discusses the VanityPath Manager tool. This was the second place submission for the 2015 AEM code contest.   

A special thank you to Lokesh BS, a top AEM community member, for contributing AEM code that is used in this article. 

This article covers many AEM concepts that are important to understand as an AEM developer. This article covers the following concepts. 

  • How to use Angular and Bootstrap JS lib files in an AEM project
  • How to invoke a Sling Servlet using an Angular HTTP call.
  • How to customize the AEM TouchUI admin user interface.
  • How to add a new view for AEM admin. 
  • How to work with a com.day.cq.commons.TidyJSONWriter object.

This article uses an Adobe Maven Archetype project to build an OSGi bundle. If you are not familiar with an Adobe Maven Archetype project, it is recommended that you read the following article: Creating your first AEM Service using an Adobe Maven Archetype project.

Digital Marketing Solution(s) Adobe Experience Manager (Adobe CQ)
Audience
Developer (Advanced)
Required Skills
Java, Angular, JSON, HTML
AEM Version(s) Adobe AEM 6.1

Note:

You can download an AEM package that contains the Vanity Manager tool. Install the package by using AEM package manager. 

 

* vanityUrl-manager-1.0.zip
An AEM package that contain the Vanity URL Manager tool

Introduction

You define Vanity URLs in Adobe Experience Manager (AEM) to create easy-to-remember and easy-to-search URLs on your website. This article steps you through how to build a custom tool that acts as a vanity path manager that easily lets you work with AEM vanity URLs. Using this tool, you can filter by site and perform other operations such as deleting them.

Intro2
The VanityPath Manager tool

Using this view, you can either delete a vanity URL or modify by clicking on it. Then you can modify the name and save it, as shown in this illustration. 

modURL
Modifying a vanity URL

sling:vanityPath

A vanity path is defined by a property named sling:vanityPath. That is, on any content in the AEM JCR, you can set a sling:vanityPath property with a short name, as shown in this illustration of the /content/geometrixx-outdoors/jcr:content node. 
 

vanURLProp
The sling:vanityPath property defined for the geometrixx-outdoors site

Modifying the TouchUI admin user interface

This article also discusses how to customize the AEM TouchUI admin user interface. A new menu item is added under Tools, as shown in the following illustration. When an AEM user clisk on Vanity URL Manager, the tool appears in a browser. 

ToolMenu
A custom menu option under AEM Tools menu

Adding Vanity URLs

The VanityPath Manager lets you add new vanity URLs to an AEM site. For example, you can add a new vanity URL to the geometrixx-outdoors site, as shown in the following illustration.

 

geoout
A new vanity URL added to an AEM site

Sling Servlets

The VanityPath Manager tool uses two Sling Servlets: 

  • SitesListImpl - used to retrieve data for the adding vanity URLs to an AEM site. This servlet also reads JCR nodes under content and returns JSON data that is used in the view under Add (code is shown later in this article).
  • VanityUrlUtilImpl - used to retrieve data for the view under Manage. This view lets you delete vanity URLs or modify the vanity URL and save it. This servlets supports GET and POST operations (code is shown later in this development article). 

JSON data

The VanityPath Manager tool uses JSON data to send data between the client web page and Sling Servlets. The following JSON is returned by a Sling Servlet in response to an HTTP GET request.  

{  
   "geometrixx":{  
      "isVanityDup":"false",
      "Site":"geometrixx",
      "Title":"Geometrixx Demo Site",
      "Path":"/content/geometrixx",
      "VanityUrl":[  
         "/geohome"
      ]
   },
   "geometrixx-outdoors":{  
      "isVanityDup":"false",
      "Site":"geometrixx-outdoors",
      "Title":"Geometrixx Outdoors Site",
      "Path":"/content/geometrixx-outdoors",
      "VanityUrl":[  
         "/foo"
      ]
   },
   "geometrixx-gov":{  
      "isVanityDup":"false",
      "Site":"geometrixx-gov",
      "Title":"Geometrixx Gov",
      "Path":"/content/geometrixx-gov",
      "VanityUrl":[  
         "/geogov"
      ]
   }
}

Install the VanityPath Manager package

Install the AEM package that is shown at the start of this community article.  For information, see INSTALLING PACKAGES.

Once you install the package, you can view the following project files. 

projectFilesA
AEM Clientlib files required for the VanityPath Manager tool

As shown in the previous illustration, the ClientLibs folder contains the required JS and CSS files required for this tool. Notice that there are Bootstrap and Angular files in this ClientLib folder. 

The following location shows the JCR node that is responsible for adding a new menu option under the Tools menu. 

ProjectFilesB
JCR nodes that create a new menu option under Tools

The /nav/tools/vanity-url node is an nt:unstructured with the following properties:

  • href - specifies the web page that is rendered when the menu option is clicked. In this example, /etc/vanity-url.html is specified. 
  • jcr:description - provides a description of the menu item
  • jcr:title - the title that appears. In this example, it is Vanity URL Manager.

 

The href property points to a page located at /etc/vanity-url. This node was a child at /etc/vanity-url/jcr:content. The sling:resourceType of this child node is vanitypath-manager/components/vanity-url-manager. This is where you will find the script that creates the AE Mweb page that represents the VanityURL Manager tool. 

Note:

When the user clicks the Vanity URL Manager menu item, the script located at is executed: /apps/vanitypath-manager/components/vanity-url-manager/vanity-url-manager.jsp

Setup Maven in your development environment

You can use Maven to build an OSGi bundle that the Vanity Path Manager tool uses. It is not required to build and deploy the OSGi bundle as it is deployed when you install the AEM package. However, in case you want to modify the OSGi bundle, the article discusses how to build it. 

Maven manages required JAR files that a Java project needs in its class path. Instead of searching the Internet trying to find and download third-party JAR files to include in your project’s class path, Maven manages these dependencies for you.

You can download Maven 3 from the following URL:

http://maven.apache.org/download.html

After you download and extract Maven, create an environment variable named M3_HOME. Assign the Maven install location to this environment variable. For example:

C:\Programs\Apache\apache-maven-3.0.4

Set up a system environment variable to reference Maven. To test whether you properly setup Maven, enter the following Maven command into a command prompt:

%M3_HOME%\bin\mvn -version

This command provides Maven and Java install details and resembles the following message:

OS name: "windows 7", version: "6.1", arch: "amd64", family: "windows"

 

Note:

For more information about setting up Maven and the Home variable, see: Maven in 5 Minutes.

Next, copy the Maven configuration file named settings.xml from [install location]\apache-maven-3.0.4\conf\ to your user profile. For example, C:\Users\scottm\.m2\.

You have to configure your settings.xml file to use Adobe’s public repository. For information, see Adobe Public Maven Repository at http://repo.adobe.com/.

The following XML code represents a settings.xml file that you can use.

<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
 | This is the configuration file for Maven. It can be specified at two levels:
 |
 |  1. User Level. This settings.xml file provides configuration for a single user, 
 |                 and is normally provided in ${user.home}/.m2/settings.xml.
 |
 |                 NOTE: This location can be overridden with the CLI option:
 |
 |                 -s /path/to/user/settings.xml
 |
 |  2. Global Level. This settings.xml file provides configuration for all Maven
 |                 users on a machine (assuming they're all using the same Maven
 |                 installation). It's normally provided in 
 |                 ${maven.home}/conf/settings.xml.
 |
 |                 NOTE: This location can be overridden with the CLI option:
 |
 |                 -gs /path/to/global/settings.xml
 |
 | The sections in this sample file are intended to give you a running start at
 | getting the most out of your Maven installation. Where appropriate, the default
 | values (values used when the setting is not specified) are provided.
 |
 |-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ~/.m2/repository
  <localRepository>/path/to/local/repo</localRepository>
  -->

  <!-- interactiveMode
   | This will determine whether maven prompts you when it needs input. If set to false,
   | maven will use a sensible default value, perhaps based on some other setting, for
   | the parameter in question.
   |
   | Default: true
  <interactiveMode>true</interactiveMode>
  -->

  <!-- offline
   | Determines whether maven should attempt to connect to the network when executing a build.
   | This will have an effect on artifact downloads, artifact deployment, and others.
   |
   | Default: false
  <offline>false</offline>
  -->

  <!-- pluginGroups
   | This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
   | when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
   | "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
   |-->
  <pluginGroups>
    <!-- pluginGroup
     | Specifies a further group identifier to use for plugin lookup.
    <pluginGroup>com.your.plugins</pluginGroup>
    -->
  </pluginGroups>

  <!-- proxies
   | This is a list of proxies which can be used on this machine to connect to the network.
   | Unless otherwise specified (by system property or command-line switch), the first proxy
   | specification in this list marked as active will be used.
   |-->
  <proxies>
    <!-- proxy
     | Specification for one proxy, to be used in connecting to the network.
     |
    <proxy>
      <id>optional</id>
      <active>true</active>
      <protocol>http</protocol>
      <username>proxyuser</username>
      <password>proxypass</password>
      <host>proxy.host.net</host>
      <port>80</port>
      <nonProxyHosts>local.net|some.host.com</nonProxyHosts>
    </proxy>
    -->
  </proxies>

  <!-- servers
   | This is a list of authentication profiles, keyed by the server-id used within the system.
   | Authentication profiles can be used whenever maven must make a connection to a remote server.
   |-->
  <servers>
    <!-- server
     | Specifies the authentication information to use when connecting to a particular server, identified by
     | a unique name within the system (referred to by the 'id' attribute below).
     | 
     | NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are 
     |       used together.
     |
    <server>
      <id>deploymentRepo</id>
      <username>repouser</username>
      <password>repopwd</password>
    </server>
    -->
    
    <!-- Another sample, using keys to authenticate.
    <server>
      <id>siteServer</id>
      <privateKey>/path/to/private/key</privateKey>
      <passphrase>optional; leave empty if not used.</passphrase>
    </server>
    -->
  </servers>

  <!-- mirrors
   | This is a list of mirrors to be used in downloading artifacts from remote repositories.
   | 
   | It works like this: a POM may declare a repository to use in resolving certain artifacts.
   | However, this repository may have problems with heavy traffic at times, so people have mirrored
   | it to several places.
   |
   | That repository definition will have a unique id, so we can create a mirror reference for that
   | repository, to be used as an alternate download site. The mirror site will be the preferred 
   | server for that repository.
   |-->
  <mirrors>
    <!-- mirror
     | Specifies a repository mirror site to use instead of a given repository. The repository that
     | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
     | for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
     |
    <mirror>
      <id>mirrorId</id>
      <mirrorOf>repositoryId</mirrorOf>
      <name>Human Readable Name for this Mirror.</name>
      <url>http://my.repository.com/repo/path</url>
    </mirror>
     -->
  </mirrors>
  
  <!-- profiles
   | This is a list of profiles which can be activated in a variety of ways, and which can modify
   | the build process. Profiles provided in the settings.xml are intended to provide local machine-
   | specific paths and repository locations which allow the build to work in the local environment.
   |
   | For example, if you have an integration testing plugin - like cactus - that needs to know where
   | your Tomcat instance is installed, you can provide a variable here such that the variable is 
   | dereferenced during the build process to configure the cactus plugin.
   |
   | As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
   | section of this document (settings.xml) - will be discussed later. Another way essentially
   | relies on the detection of a system property, either matching a particular value for the property,
   | or merely testing its existence. Profiles can also be activated by JDK version prefix, where a 
   | value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
   | Finally, the list of active profiles can be specified directly from the command line.
   |
   | NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
   |       repositories, plugin repositories, and free-form properties to be used as configuration
   |       variables for plugins in the POM.
   |
   |-->
  <profiles>
    <!-- profile
     | Specifies a set of introductions to the build process, to be activated using one or more of the
     | mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
     | or the command line, profiles have to have an ID that is unique.
     |
     | An encouraged best practice for profile identification is to use a consistent naming convention
     | for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
     | This will make it more intuitive to understand what the set of introduced profiles is attempting
     | to accomplish, particularly when you only have a list of profile id's for debug.
     |
     | This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
    <profile>
      <id>jdk-1.4</id>

      <activation>
        <jdk>1.4</jdk>
      </activation>

      <repositories>
        <repository>
          <id>jdk14</id>
          <name>Repository for JDK 1.4 builds</name>
          <url>http://www.myhost.com/maven/jdk14</url>
          <layout>default</layout>
          <snapshotPolicy>always</snapshotPolicy>
        </repository>
      </repositories>
    </profile>
    -->

    <!--
     | Here is another profile, activated by the system property 'target-env' with a value of 'dev',
     | which provides a specific path to the Tomcat instance. To use this, your plugin configuration
     | might hypothetically look like:
     |
     | ...
     | <plugin>
     |   <groupId>org.myco.myplugins</groupId>
     |   <artifactId>myplugin</artifactId>
     |   
     |   <configuration>
     |     <tomcatLocation>${tomcatPath}</tomcatLocation>
     |   </configuration>
     | </plugin>
     | ...
     |
     | NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
     |       anything, you could just leave off the <value/> inside the activation-property.
     |
    <profile>
      <id>env-dev</id>

      <activation>
        <property>
          <name>target-env</name>
          <value>dev</value>
        </property>
      </activation>

      <properties>
        <tomcatPath>/path/to/tomcat/instance</tomcatPath>
      </properties>
    </profile>
    -->
  

<profile>

                <id>adobe-public</id>

                <activation>

                    <activeByDefault>true</activeByDefault>

                </activation>

                <repositories>

                  <repository>

                    <id>adobe</id>

                    <name>Nexus Proxy Repository</name>

                    <url>http://repo.adobe.com/nexus/content/groups/public/</url>

                    <layout>default</layout>

                  </repository>

                </repositories>

                <pluginRepositories>

                  <pluginRepository>

                    <id>adobe</id>

                    <name>Nexus Proxy Repository</name>

                    <url>http://repo.adobe.com/nexus/content/groups/public/</url>

                    <layout>default</layout>

                  </pluginRepository>

                </pluginRepositories>

            </profile>

</profiles>

  <!-- activeProfiles
   | List of profiles that are active for all builds.
   |
  <activeProfiles>
    <activeProfile>alwaysActiveProfile</activeProfile>
    <activeProfile>anotherAlwaysActiveProfile</activeProfile>
  </activeProfiles>
  -->
</settings>

Create an Experience Manager archetype project 

You can create an Experience Manager archetype project by using the Maven archetype plugin. In this example, assume that the working directory is C:\AdobeCQ.

plugin1

To create an Experience Manager archetype project, perform these steps:

  1. Open the command prompt and go to your working directory (for example, C:\AdobeCQ).

  2. Run the following Maven command:

    mvn archetype:generate -DarchetypeRepository=https://repo.adobe.com/nexus/content/groups/public/ -DarchetypeGroupId=com.day.jcr.vault -DarchetypeArtifactId=multimodule-content-package-archetype -DarchetypeVersion=1.0.2 -DgroupId=com.mindtree.vanitypath.servlets -DartifactId=vanity -Dversion=1.0-SNAPSHOT -Dpackage=com.mindtree.vanitypath.servlets -DappsFolderName=myproject -DartifactName="My Project" -DcqVersion="5.6.1" -DpackageGroup="My Company"
  3. When prompted for additional information, specify Y.

  4. Once done, you will see a message like:
    [INFO] Finished at: Wed Mar 27 13:38:58 EDT 2013
    [INFO] Final Memory: 10M/184M

  5. Change the command prompt to the generated project. For example: C:\AdobeCQ\vanity. Run the following Maven command:
    mvn eclipse:eclipse

After you run this command, you can import the project into Eclipse as discussed in the next section.

Add Java files to the Maven project using Eclipse 

To make it easier to work with the Maven generated project, import it into the Eclipse development environment, as shown in the following illustration.

project

Note:

Delete all packages other than com.mindtree.vanitypath.servlets. Next make sure to delete all generated JAVA files. Make sure that the only Java files in this package are these ones:

  • SitesListImpl- the servlet that populate the VanityPath Manager tool under the Add view
  • VanityUrlUtilImpl - the servlet that populates the tool under the Manage view

SitesListImpl

The SitesListImpl servlet defines both a doPost and doGet methods: 

  • doGet- this method reads the nodes under /content.  A TidyJSONWriter onject is created that is responsible for storing JSON data that contains JCR data. The JSON data is used to populae the controls under the Add view. 
  • doPost- this method creates a new vanity URL for the JCR node that is posted. That is, when a user enter a new vanity URL in the Add view, the new vanity URL is posted to this servlet. 

The following code represents the SitesListImpl method. 

package com.mindtree.vanitypath.servlets;

import java.io.IOException;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.nodetype.NodeType;
import javax.servlet.ServletException;

import org.apache.felix.scr.annotations.Reference;
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.ResourceResolverFactory;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.jcr.api.SlingRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.commons.TidyJSONWriter;


//public class VanityUrlUtilImpl implements VanityUrlUtil{
@SlingServlet(paths={"/services/siteslist"}, methods={"GET"})
	public class SitesListImpl extends SlingAllMethodsServlet {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	@Reference
	private ResourceResolverFactory resolverFactory;
	
	@Reference
	private SlingRepository repo;
	
	private final Logger log = LoggerFactory.getLogger(SitesListImpl.class);
	
	@Override
	public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException{ 

		log.info("####### Inside SitesListImpl ########");
		Session session = null;
		TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());	

		try{
			tidyJSONWriter.array();			
			session = repo.login(new SimpleCredentials("admin", "admin".toCharArray()));
			Node root = session.getRootNode();
			Node currentNode = root.getNode("content");
			getNodes(currentNode, tidyJSONWriter);
			           
            tidyJSONWriter.endArray();
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");            
            

		}catch(Exception e){
			log.error(e.getMessage());
		}finally{
			if(session.isLive()){
				session.logout();
			}
		}	

		
	}
	
	public void getNodes(Node path, TidyJSONWriter tidyJSONWriter) {
		try{
			NodeIterator itr = path.getNodes();
			
			while (itr.hasNext()) {
				Node node = itr.nextNode();
				NodeType nt = node.getPrimaryNodeType();
				if(nt.isNodeType("cq:Page")){
					log.info(" Node Type : " + nt);
					log.info(" Node Path : " + node.getPath());
					
					String[] sites = node.getPath().split("/");
					String site = sites[2];
					
					tidyJSONWriter.object();
					
					tidyJSONWriter.key("title").value(node.getName());
					tidyJSONWriter.key("path").value(node.getPath());
					tidyJSONWriter.key("site").value(site);
					tidyJSONWriter.key("collapsed").value(true);
					tidyJSONWriter.key("child");
					tidyJSONWriter.array();
					if(node.hasNodes()){					
						getNodes(node, tidyJSONWriter);										
					}
					tidyJSONWriter.endArray();
					tidyJSONWriter.endObject();		
					
				}	
	        }
			
		}catch(Exception e){
				log.error(e.getMessage());
		}
	    
	}
		
	

}

VanityUrlUtilImpl

The VanityUrlUtilImpl servlet defines both a doPost and doGet methods: 

  • doGet- this method reads the AEM JCR nodes to prepare JSON data for the Manage view. The JSON data is used to display the existign vanity URLs.  
  • doPost- this method accepts POST request to modify the name of a vanity URL under the Manage view. That is, when a user changes the name of a vanity URL under the Manage view and the data is sent within a POST request. 

The following code represents the VanityUrlUtilImpl method. 

package com.mindtree.vanitypath.servlets;

import java.io.IOException;
import java.util.HashMap;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.qom.Constraint;
import javax.jcr.query.qom.DynamicOperand;
import javax.jcr.query.qom.QueryObjectModel;
import javax.jcr.query.qom.QueryObjectModelConstants;
import javax.jcr.query.qom.QueryObjectModelFactory;
import javax.jcr.query.qom.Selector;
import javax.jcr.query.qom.StaticOperand;
import javax.servlet.ServletException;

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.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.commons.TidyJSONWriter;
import com.day.cq.wcm.api.Page;

@SlingServlet(paths={"/services/vanityurlcheck"}, methods={"GET","POST"})
	public class VanityUrlUtilImpl extends SlingAllMethodsServlet {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private final Logger log = LoggerFactory.getLogger(VanityUrlUtilImpl.class);
	
	String rootPath = "/content/";
	
	@Override
	public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException{ 

		log.info("####### Inside VanityUrlUtilImpl ########");		
		String check = request.getParameter("validate");
		if(check != null && check.equals("true")){			
				getVanityPath(request, response);				
		}else{
				getAllVanityPaths(request, response); 	
		}
		
	}


	private void getAllVanityPaths(SlingHttpServletRequest request, SlingHttpServletResponse response){

		Session session = request.getResourceResolver().adaptTo(Session.class);
		ResourceResolver rr = request.getResourceResolver();
		
		try{
			NodeIterator nodes = getVanityPaths(session, rootPath, null);
			HashMap<String,String> duplicateVanityList = getDuplicateVanity(session, rr);
	
			TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());			 
			tidyJSONWriter.object();
	
			for (int i = 0; i < nodes.getSize(); i++) {
				Node node = nodes.nextNode();
				Page page = getPageObject(rr, node);
				String site = getSite(page);
				
			    tidyJSONWriter.key(page.getName());			    
			    tidyJSONWriter.object();
			    
			    if(!duplicateVanityList.isEmpty()){
			    	log.info("####### duplicate Key ######## : "+ duplicateVanityList.get(site+page.getVanityUrl()));
			        if(duplicateVanityList.containsKey(site+page.getVanityUrl())){
			        	tidyJSONWriter.key("isVanityDup").value("true");
					}else{
						tidyJSONWriter.key("isVanityDup").value("false");
					}
			    }else{
			    	tidyJSONWriter.key("isVanityDup").value("false");
			    }
			    tidyJSONWriter.key("Site").value(site);
				tidyJSONWriter.key("Title").value(page.getTitle());
				tidyJSONWriter.key("Path").value(page.getPath());
				tidyJSONWriter.key("VanityUrl");
				tidyJSONWriter.array();
				String vanityUrls = "";				
				Property vanityPath = node.getProperty("sling:vanityPath");				
				if(vanityPath.isMultiple()){
					Value[] values = vanityPath.getValues();
					for (int j = 0; j < values.length; j++) {
						log.info("####### vanityUrls ######## : "+ values.length + j);
						tidyJSONWriter.value(values[j]);
						vanityUrls = vanityUrls+values[j]+",";
					}					
				}else{
					vanityUrls = page.getVanityUrl();
					tidyJSONWriter.value(page.getVanityUrl());
				}
				
				tidyJSONWriter.endArray();
				
				String redirectType = null;
				if(node.hasProperty("sling:redirect")){
					String redirect = node.getProperty("sling:redirect").getString();
					log.info("####### redirect ######## : "+ redirect);
					if(node.hasProperty("sling:redirectStatus")){
						String redirectStatus = node.getProperty("sling:redirectStatus").getString();
						log.info("####### redirectStatus ######## : "+ redirectStatus);
						redirectType = redirectStatus;
					}else{
						redirectType = "301";
					}
				}else{
					redirectType = "vanity";
				}					
				
				tidyJSONWriter.key("RedirectType").value(redirectType);
				
			    tidyJSONWriter.endObject();				
			}
	
			tidyJSONWriter.endObject();
			
			response.setContentType("application/json");
			response.setCharacterEncoding("UTF-8");
		}catch(Exception e){
			log.error(e.getMessage());
		}finally{
			if(session.isLive()){
				session.logout();
			}
			if(rr.isLive()){
				rr.close();
			}
		}	
	}
	

	private void getVanityPath(SlingHttpServletRequest request, SlingHttpServletResponse response){
		Session session = request.getResourceResolver().adaptTo(Session.class);  
		try{                      
            log.info("Session UserId : "+  session.getUserID());
            final String vanityPath = request.getParameter("vanityPath");
            final String pagePath = request.getParameter("pagePath");
            log.info("vanity path parameter passed is {}", vanityPath);
            log.info("page path parameter passed is {}", pagePath);
            try {

                NodeIterator nodes = getVanityPaths(session, rootPath, vanityPath);
 
                TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter()); 
                tidyJSONWriter.object(); 
                tidyJSONWriter.key("vanitypaths").array(); 
                String pageSite = getSite(pagePath);;
                String nodeSite = null;
                while (nodes.hasNext()) {
                    Node node = nodes.nextNode();
                    log.info("Node path is {}", node.getPath());
                    log.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.
                    	nodeSite = getSite(node.getPath());
                        if(node.getPath().equals(pagePath) || !(nodeSite.equals(pageSite)))
                        {
                            //do not add that to the list
                        	log.info("Node path is {}", node.getPath());
                        	log.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){
            	log.error( "Error in doGet", re );
            }
        }catch (JSONException e) {
        	log.error( "Error in doGet", e );
        }catch(Exception e){
			log.error(e.getMessage());
		}finally{
			if(session.isLive()){
				session.logout();
			}
			
		}	
	}

	/**
	 * @param page
	 * @return
	 */
	private String getSite(Page page) {
		String[] sites = page.getPath().split("/");
		String site = sites[2];
		log.info("### site  : " + site);
		return site;
	}
	
	/**
	 * @param page
	 * @return
	 */
	private String getSite(String pagePath) {
		String[] sites = pagePath.split("/");
		String site = sites[2];
		log.info("### site  : " + site);
		return site;
	}
	
	/**
	 * @param rr
	 * @param node
	 * @return
	 * @throws RepositoryException
	 */
	private Page getPageObject(ResourceResolver rr, Node node) throws RepositoryException {
		log.info("##### NOde Path ####### : " + node.getPath() + " : " + node.getPath().replace("/jcr:content", ""));
		Resource resource = rr.getResource(node.getPath().replace("/jcr:content", ""));
		Page page = resource.adaptTo(Page.class);
		return page;
	}

	private HashMap<String, String> getDuplicateVanity(Session session, ResourceResolver rr) throws RepositoryException {
		HashMap<String, String> duplicateVanity = new HashMap<String, String>();
		HashMap<String, String> map = new HashMap<String, String>();
		NodeIterator itr = getVanityPaths(session, rootPath, null);
		for (int i = 0; i < itr.getSize(); i++) {
			Node node = itr.nextNode();
			Page page = getPageObject(rr, node);
			String site = getSite(page);
			if(map.containsKey(site+page.getVanityUrl())){
				duplicateVanity.put(site+page.getVanityUrl(), page.getVanityUrl());
			}else{
				map.put(site+page.getVanityUrl(), page.getVanityUrl());
			}					
		}
		return duplicateVanity;
	}

	/**
	 * @param session
	 * @return
	 * @throws RepositoryException
	 * @throws PathNotFoundException
	 * @throws UnsupportedRepositoryOperationException
	 * @throws InvalidQueryException
	 */
	private NodeIterator getVanityPaths(Session session, String path, String vanityPath) throws RepositoryException, PathNotFoundException,
			UnsupportedRepositoryOperationException, InvalidQueryException {
		log.info("####### session ######### : " + session.getUserID());
		Node root = session.getRootNode();
		Node currentNode = root.getNode("content");
		QueryObjectModelFactory qf = currentNode.getSession().getWorkspace().getQueryManager().getQOMFactory();
		Selector selector = qf.selector("nt:base", "s");		
		Constraint constriant = qf.descendantNode("s", path);
		constriant = qf.and(constriant, qf.propertyExistence("s", "sling:vanityPath"));
		if(vanityPath != null){
			ValueFactory valueFactory = session.getValueFactory();
			String operator  = QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO; 
			DynamicOperand dynOperand  = qf.propertyValue("s", "sling:vanityPath"); 
		    StaticOperand statOperand  = qf.literal(valueFactory.createValue(vanityPath)); 
			constriant = qf.and(constriant, qf.comparison(dynOperand , operator, statOperand));	
		}
		QueryObjectModel qm = qf.createQuery(selector, constriant, null, null);
		log.info("######### Query ######### : " + qm.getStatement());
		NodeIterator nodes = qm.execute().getNodes();
		log.info("### query result : " + nodes.getSize());
		return nodes;
	}
	
	@Override
	public void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException{ 
		String operation = request.getParameter("op");
		String path = request.getParameter("path");
		String pageSite = getSite(path);
		log.error("######## path ######## : " + path);	
		Session session = null;
		try{
			TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());			 
            tidyJSONWriter.object();
            session=request.getResourceResolver().adaptTo(Session.class);
            ValueFactory valueFactory = session.getValueFactory();
			//session = repo.login(new SimpleCredentials("admin", "admin".toCharArray()));
			if(operation.equals("update")){
				String vanityurl = request.getParameter("vanityurl");
				String redirectType = request.getParameter("redirectType");
				log.error("######## vanityurl ########" + vanityurl + " : " + request.getParameter("vanityurl"));	
				String site = rootPath+request.getParameter("site");
				String[] vanitypaths = vanityurl.split(",");
				Value[] values = new Value[vanitypaths.length];
				boolean isDup = false;
				String nodeSite = "";
				for (int i = 0; i < vanitypaths.length; i++) {
					values[i] = valueFactory.createValue(vanitypaths[i]);
					NodeIterator nodes = getVanityPaths(session, site, values[i].getString());
					if(nodes.getSize() > 0){
						 while (nodes.hasNext()) {
			                    Node node = nodes.nextNode();
			                    nodeSite = getSite(node.getPath());
			                    log.error("######## node path ######## : " + node.getPath());	
			                    if(!(node.getPath().replace("/jcr:content", "").equals(path))  && (nodeSite.equals(pageSite)))
			                    {
			                    	isDup = true;
			                    	log.error("######## Vanity Path Duplication ########");					
			                    	tidyJSONWriter.key("Error").value("This Vanity Path already exists in this Site");
			                    }
						 }
					}
				}				
				if(!isDup){
					updateVanityUrl(session, path, values, redirectType);
					tidyJSONWriter.key("Success").value("Added/Updated Successfully");
				}
				
			}else{
				deleteVanityUrl(session, path);
				tidyJSONWriter.key("Error").value("Deleted Successfully"); 
			}
			session.save();
			tidyJSONWriter.endObject();
			response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
			
		}catch(Exception e){
			log.error(e.getMessage());
		}finally{
			if(session.isLive()){
				session.logout();
			}
		}	
		
	}

	private void updateVanityUrl(Session session, String path, Value[] vanityurl, String redirectType) throws PathNotFoundException, RepositoryException {
		log.info("####### Inside updateVanityUrl ########");

			Node currentNode = session.getNode(path);
			currentNode.getNode("jcr:content").setProperty("sling:vanityPath", vanityurl);
			if(!redirectType.equals("1")){
				currentNode.getNode("jcr:content").setProperty("sling:redirect", true);
				currentNode.getNode("jcr:content").setProperty("sling:redirectStatus", redirectType);
			}
	
	}

	private void deleteVanityUrl(Session session, String path) throws  PathNotFoundException, RepositoryException {
		log.info("####### Inside deleteVanityUrl ########");
		String[] vanityUrls = null;
		Node currentNode = session.getNode(path);
		currentNode.getNode("jcr:content").setProperty("sling:vanityPath", vanityUrls);
		String v = null;
		if(currentNode.getNode("jcr:content").hasProperty("sling:redirect")){
			currentNode.getNode("jcr:content").setProperty("sling:redirect", v);
		}
		if(currentNode.getNode("jcr:content").hasProperty("sling:redirectStatus")){
			currentNode.getNode("jcr:content").setProperty("sling:redirectStatus", v);
		}	
		
			
		
	}
	

}

Modify the Maven POM file 

Modify the POM file to successfully build the OSGi bundle. In the POM file located at C:\AdobeCQ\quiz2\bundle, add the following dependencies.

  • org.apache.felix.scr
  • org.apache.felix.scr.annotations
  • org.apache.jackrabbit
  • org.apache.sling

The following XML represents this POM file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd ">
    <modelVersion>4.0.0</modelVersion>
    <!-- ====================================================================== -->
    <!-- P A R E N T P R O J E C T D E S C R I P T I O N -->
    <!-- ====================================================================== -->
    <parent>
        <groupId>com.mindtree.vanitypath.servlets</groupId>
        <artifactId>vanity</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <!-- ====================================================================== -->
    <!-- P R O J E C T D E S C R I P T I O N -->
    <!-- ====================================================================== -->

    <artifactId>vanity-bundle</artifactId>
    <packaging>bundle</packaging>
    <name>My Project Bundle</name>

   <dependencies>
        
       <dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.api</artifactId>
    <version>2.3.0</version>
    <scope>provided</scope>
</dependency>
        
           <dependency>
        <groupId>com.adobe.aem</groupId>
        <artifactId>aem-api</artifactId>
        <version>6.0.0.1</version>
        <scope>provided</scope>
    </dependency>
         
         
   <dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.api</artifactId>
    <version>2.8.0</version>
</dependency>
  
         
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.compendium</artifactId>
        </dependency>
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.felix</groupId>
            <artifactId>org.apache.felix.scr.annotations</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
           
            
        <dependency>
         <groupId>org.apache.felix</groupId>
     
         <artifactId>org.osgi.core</artifactId>
     
         <version>1.4.0</version>
      </dependency>
           
          
            
    <dependency>
    <groupId>org.apache.jackrabbit</groupId>
    <artifactId>jackrabbit-core</artifactId>
    <version>2.4.3</version>
    </dependency>
         
    <dependency>
    <groupId>org.apache.jackrabbit</groupId>
    <artifactId>jackrabbit-jcr-commons</artifactId>
    <version>2.4.3</version>
    </dependency>
   <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
             
     
           
      <dependency>
         <groupId>javax.jcr</groupId>
         <artifactId>jcr</artifactId>
         <version>2.0</version>
      </dependency>
         
       <dependency>
            <groupId>com.day.cq.wcm</groupId>
            <artifactId>cq-wcm-api</artifactId>
            <version>5.5.0</version>
            <scope>provided</scope>
        </dependency>
           
        <dependency>
            <groupId>com.day.cq</groupId>
            <artifactId>cq-commons</artifactId>
            <version>5.5.0</version>
            <scope>provided</scope>
        </dependency>
         
        <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
</dependency>
          
                  
    </dependencies>

    <!-- ====================================================================== -->
    <!-- B U I L D D E F I N I T I O N -->
    <!-- ====================================================================== -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-scr-descriptor</id>
                        <goals>
                            <goal>scr</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>com.mindtree.vanitypath.servlets.vanity-bundle</Bundle-SymbolicName>
                    </instructions>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.sling</groupId>
                <artifactId>maven-sling-plugin</artifactId>
                <configuration>
                    <slingUrl>http://${crx.host}:${crx.port}/apps/myproject/install</slingUrl>
                    <usePut>true</usePut>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                 <configuration>
                    <excludePackageNames>
                        *.impl
                    </excludePackageNames>
                 </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Build the OSGi bundle using Maven 

To build the OSGi bundle by using Maven, perform these steps:

  1. Open the command prompt and go to the C:\AdobeCQ\quiz2 folder.

  2. Run the following maven command: mvn clean install.

  3. The OSGi component can be found in the following folder: C:\AdobeCQ\vanity\bundle\target. The file name of the OSGi component is vanity-bundle-1.0-SNAPSHOT.jar.

Deploy the bundle to Experience Manager

Deploy the OSGi bundle by performing these steps:

  1. Login to Apache Felix Web Console at http://server:port/system/console/bundles (default admin user = admin with password= admin).

  2. Click the Bundles tab, sort the bundle list by Id, and note the Id of the last bundle.

  3. Click the Install/Update button.

  4. Browse to the bundle JAR file you just built using Maven. (C:\AdobeCQ\vanity\bundle\target).

  5. Click Install.

  6. Click the Refresh Packages button.

  7. Check the bundle with the highest Id.

  8. Click Active. Your new bundle should now be listed with the status Active.

  9. If the status is not Active, check the CQ error.log for exceptions.

Understanding VanityManager Script

The Vanity Manager consists of a JSP file located here:

/apps/vanitypath-manager/components/vanity-url-manager/vanity-url-manager.jsp

Notice the table in the Vanity Manager web page (under the Manager tab), as shown here. 

Intro2
The Vanity Manager Tool

This is defined in the vanity-url-manager.jsp by using an Angular markup table element. For information, see AngularJS Tables.

 <table class="data index-table ng-cloak">
                                    <tr ng-if="(!indexes || indexes.length === 0)" colspan="4">
                                        <td>No data found</td>
                                      </tr>
                                    <thead>
                                        <tr>
                                            <th></th>
                                            <th>Site</th>
                                            <th>Page Title</th>
                                            <th>Vanity URL</th>
                                            <th>Remove</th>
        
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr ng-repeat="index in filtered = (indexes | siteFilter:selectedSite)" >
											<td>
                                                <div ng-if="(index.isVanityDup == 'true')" >
                                                    <span class="icon-warning-sign yellow" tooltip="Duplicate Vanity URL"></span>
                                                </div>
                                            </td>        
                                            <td>{{ index.Site }}</td>
        
                                            <td>{{ index.Title }}</td>
        
                                            <td>
                                                <div ng-repeat="vanity in index.VanityUrl track by $index">
                                                    <a ng-href="{{vanity}}.html" editable-text="index.VanityUrl[$index]" onaftersave="saveVanity(index.VanityUrl, index.Path, index.Site)">{{vanity}}</a>
                                                </div>

                                            </td>
        
                                             <td style="white-space: nowrap">

                                                <div class="buttons" ng-show="!rowform.$visible">
                                                  <!--<button class="btn btn-primary" ng-click="rowform.$show()">edit</button> -->
                                                  <button class="btn btn-danger" ng-click="removeVanity($index)">del</button>
                                                </div>  
                                              </td>      
        
                                        </tr>     
        
                                    </tbody>
                                </table>

Table in the Add view

Likewise, notice the table under the Add view, as shown in this illustration. 

Intro2
The Table under the Add view

This table is defined in the vanity-url-manager.jsp by using an Angular markup table element. 

  <table ng-if="(currentNode.path != null)" class="data index-table ng-cloak">
                                                <thead>
                                                    <tr>
                                                       <!-- <th class="check"><label><input type="checkbox" ng-model="toggleChecks"><span></span></label></th> -->
                                                        <th>Site</th>
                                                        <th>Page Path</th>
                                                        <th>Page Title</th>
                                                        <th>Vanity URL</th>
                                                        <th>Edit</th>        
                                                    </tr>
                                                </thead>
                                                <tbody>
                                                    <tr> 
                                                        <td> 
                                                            <span e-name="Site" e-form="addform" >
                                                                {{currentNode.site}}
                                                            </span>        
                                                        </td>
                                                        <td> 
                                                            <span e-name="Path" e-form="addform" >
                                                                {{currentNode.path}}
                                                            </span>        
                                                        </td>
                    
                                                        <td>
                                                            {{ currentNode.title }} 
                                                        </td>
            
                                                        <td>
                                                            <span editable-text="index.VanityUrl" e-name="VanityUrl" e-form="addform" >
                                                            </span>        
                                                        </td>
                    
                                                         <td style="white-space: nowrap">
                                                            <!-- form -->          
            
                                                            <form editable-form name="addform" onaftersave="saveVanity(index.VanityUrl, currentNode.path, currentNode.site)" ng-show="addform.$visible" class="form-buttons form-inline" shown="inserted == index">
                                                              <button type="submit" ng-disabled="addform.$waiting" class="btn btn-primary">
                                                                save
                                                              </button>
                                                              <button type="button" ng-disabled="addform.$waiting" ng-click="addform.$cancel()" class="btn btn-default">
                                                                cancel
                                                              </button>
                                                            </form>
                                                            <div class="buttons" ng-show="!addform.$visible">
                                                              <button class="btn btn-primary" ng-click="addform.$show()">add new</button>
                                                            </div>             
                                                          </td>                 
                                                    </tr>                   
                                                </tbody>
                                            </table>     

Invoking the Sling Servlets

The data that populates the tables is retrieved by using the Sling Servlets created in this section. The application logic that invokes the Servlet is located in this file: 

/apps/vanitypath-manager/components/vanity-url-manager/clientlibs/js/app.js

This file contain Angular JavaScript logic that populates controls on the web page. For example, consider the following tree control. 

tree
Tree control populated by a GET request

The following code shows a GET requets made to the /services/siteslist servlet.

$scope.treelist = function () {

			 $scope.notifications = [];
             if($scope.treesites.length == 0){
                  $scope.app.running = true;

                $http({
    
                    method: 'GET',
                    url: encodeURI('http://'+host+':'+port+'/services/siteslist'),
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'}
                }).
                    success(function (data, status, headers, config) {
    					$scope.app.running = false;
                        $scope.treesites=data;
    
                    }).
                    error(function (data, status, headers, config) {
                        $scope.addNotification('error', 'ERROR', 'Unable to get Sites');
                    });
             }

        };

Note:

It is recommended that you look at the script.js file to understand how it works. 

When you click on an item in the tree that represents an AEM site, you can enter a vanity URL. When you click the Save button, a POST request is sent to the /services/vanityurlcheck servlet. 

PostAdd
A new vanity URL

An Angular POST request made to /services/vanityurlcheck.  For information, see Angular Post

  $scope.indexes.VanityUrl=data;
			var vanityUrl =  $scope.indexes.VanityUrl;
            $http({

                method: 'POST',
                url: encodeURI('http://'+host+':'+port+'/services/vanityurlcheck'),
                  data : 'op=update&path='+id+'&vanityurl='+vanityUrl+'&site='+site,
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'}
                }).
                    success(function (data, status, headers, config) {  

                        angular.forEach(data, function (index,key) {

                           if(angular.equals(key,"Error")){
                       			$scope.addNotification('error', 'Error', index);
                           }else{
								$scope.addNotification('success', 'SUCCESS', 'Saved Successfully !');
                           }

                    	});

    
                    }).
                	error(function (data, status, headers, config) {
                        $scope.addNotification('error', 'ERROR', 'Unable to Save');
                    });
          };
        

Note:

It is recommended that you look through the rest of the JavaScript in the clientlibs/js/app.js file to understand how Angular code works with AEM.  

See also

Congratulations, you have just read about how the AEM Vanity Manager tool work and uses Angular JS framework. Please refer to the AEM community page for other articles that discuss how to build AEM services/applications.

This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License  Twitter™ and Facebook posts are not covered under the terms of Creative Commons.

Legal Notices   |   Online Privacy Policy