Article summary

Summary
Discusses the following points:
  • how to use the Package Manager API to dynamically build AEM packages. 
  • how to post data to a servlet that is registered by resource types.
  • how to use JavaScript code to update an AEM page view. 
A special thank you to David Gonzalez, a Technical Marketing Engineer, for his help during this development article.  
Digital Marketing Solution(s) Adobe Experience Manager (Adobe CQ)
Audience
Developer (intermediate - advanced)
Required Skills
Java, JCR nodes, JavaScript, HTML
Tested On Adobe Experience Manager 6.2

Introduction

You can dynamically create an Adobe Experience Manager package by using a Java API. That is, you can create an OSGi bundle that contains code that builds an AEM package. Building a package using code lets build a package without having an AEM administrator build a package manually, For example, some use cases are to build packages every week automatically. For information about the Package Manager Java API, see Interface JcrPackageManager.

This use case in this article is to show you how to copy access control entries from one environment to another without copying the actual content itself, just the access control entries. The AEM user interface allows you to define and build packages containing access control entries for particular paths and/or particular principals. Using the Java API, you can perform this use case, as shown in the following illustration.

PackageSuccess
An AEM client invokes a sling servlet that builds a package

One the AEM client successfully builds the package, you can view it within AEM Package Manager, as shown in this illustration.

PM
A package created by code is displayed within AEM package manager

Download and build the acs-commons project 

The first step is to download and build the acs-commons 6.2 project that is located from the following GitHub repository:

https://github.com/Adobe-Consulting-Services/acs-aem-commons/tree/feature/6.2-compatibility

Download the GitHub repository using this command:

git clone git://<URL TO THE REPOSITOY>

This command downloads the repository to your local Github folder. Now you can use Maven to build and deploy the acs-commons project to AEM. 

mvn -PautoInstallPackage install

This command places the acs-commons in the apps folder in your local version of AEM.  

Next, run the following Maven command.

mvn eclipse:eclipse

Now you are ready to import the project into eclipse as discussed in the next section. 

Note:

For more information about setting up Maven, see: Maven in 5 Minutes.

Import the project into Eclipse

The next step is to import the ASC-Commons project into eclipse.

eclipse
The Eclipse import dialog

This project contains a lot of packages and Java files. You do not need to change any code, but rather understand the existing code so you can apply it to your own projects. The following illustration shows the packages in Eclipse that contains the Java files used in this article. 

JavaPackages
The com.adobe.acs.commons.packaging.impl package

Note:

It is not necessary to change the Java application logic. You can use the code as is. This section explains the Java code that uses the Package Manager API. It is important you understand how it works so you can apply it to your own AEM projects.  

Understanding the Java files in the package

As shown in the previous illustration, the com.adobe.acs.commons.packaging.impl package contains Java files used in the AEM Package application. The entry point into these classes is the  ACLPackagerServletImpl sling servlet. This class extendsAbstractPackagerServlet, which in return extends SlingAllMethodsServlet.

Notice that the ACLPackagerServletImpl servlet contains a sling servlet annotation.

@SlingServlet(
        methods = { "POST" },
        resourceTypes = { "acs-commons/components/utilities/packager/acl-packager" },
        selectors = { "package" },
        extensions = { "json" }
)
public class ACLPackagerServletImpl extends AbstractPackagerServlet {

Notice that this example uses a resource type acs-commons/components/utilities/packager/acl-packager. This means that the servlet is registered by resource type. For information about different ways in which sling servlets are registered, see Servlets and Scripts

Also notice the selectors value in this example is package. Likewise, the extensions value is json. This means that these values must be present in the request URL in order for this servlet to be invoked. For example: 

URL = "requestURL"+.package.json

This is using Sling functionality to invoke the servlet.

Note:

To learn how to register a sling servlet using a resource type, see Binding Adobe Experience Manager Servlets to ResourceTypes

The following values are set by the AEM configured page (the AEM packager page discussed later in this article) and used by the sling servlet that creates the AEM package: 

  • packageName - name of the package
  • packageGroupName - name of the package
  • packageVersion - the version of the package 
  • packageDescription - package description
  • packageACLHandling - ACL values
  • conflictResolution - how to handle conflict resolution
  • principalNames - AEM users
  • includePatterns - filter and patterns required for the package
  • includePrincipals - whether to include principals
  • includeConfiguration - whether to incluide configuration information

Note:

The page's dialog saves these values using the usual AEM dialog plumbing to the underlying page’s jcr:content node. The servlet reads these values. 

The doPost method reads these values that are used to build an AEM package. In this example, the doPost method is the HTTP entry point into executing this functionality.

 final ValueMap properties = this.getProperties(request);

 final String[] principalNames = properties.get(PRINCIPAL_NAMES, new String[]{});

 final List<PathFilterSet> packageResources = this.findResources(resourceResolver,
  Arrays.asList(principalNames),
   toPatterns(Arrays.asList(properties.get(INCLUDE_PATTERNS, new String[]{}))));

  try {
        // Add Principals
        if (properties.get(INCLUDE_PRINCIPALS, DEFAULT_INCLUDE_PRINCIPALS)) {
                packageResources.addAll(this.getPrincipalResources(resourceResolver, principalNames));
            }
           

This line of code: 

  final String[] principalNames = properties.get(PRINCIPAL_NAMES, new String[]{});

returns a string array of principals that were selected from the page dialog using a multi-field control, as shown in this illustration.

dialog
The dialog of the packager page

This line of code: 

final List packageResources = this.findResources(resourceResolver,Arrays.asList(principalNames), toPatterns(Arrays.asList(properties.get(INCLUDE_PATTERNS, new String[]{}))));

invokes a method named findResources that returns a List object where each element is a PathFilterSet object.  A PathFilterSet holds a set of path filters  where each attribute is an include or exclude filter. This evaluation of the set allows included paths and rejects excluded paths. For information, see PathFilterSet

The findResources  method searches the AEM JCR for all rep:ACE nodes to be further filtered by Grant/Deny ACE rep:principalNames. The following code represents this method. 

 

 private List<PathFilterSet> findResources(final ResourceResolver resourceResolver,
                                              final List<String> principalNames,
                                              final List<Pattern> includePatterns) {
        boolean isOak = true;
        try {
            isOak = aemCapabilityHelper.isOak();
        } catch (RepositoryException e) {
            isOak = true;
        }

        final Set<Resource> resources = new TreeSet<Resource>(resourceComparator);
        final List<PathFilterSet> pathFilterSets = new ArrayList<PathFilterSet>();

        String[] queries = CQ5_QUERIES;
        if (isOak) {
            queries = AEM6_QUERIES;
        }

        for (final String query : queries) {
            final Iterator<Resource> hits = resourceResolver.findResources(query, QUERY_LANG);

            while (hits.hasNext()) {
                final Resource hit = hits.next();
                Resource repPolicy = null;

                if (isOak) {
                    // If Oak, get the parent node since the query is for the Grant/Deny nodes
                    if (hit.getParent() != null) {
                        repPolicy = hit.getParent();
                    }
                } else {
                    // If not Oak, then the rep:ACL is the hit
                    repPolicy = hit;
                }

                if (this.isIncluded(repPolicy, includePatterns)) {
                    log.debug("Included by pattern [ {} ]", repPolicy.getPath());
                } else {
                    continue;
                }

                final Iterator<Resource> aces = repPolicy.listChildren();

                while (aces.hasNext()) {
                    final Resource ace = aces.next();
                    final ValueMap props = ace.adaptTo(ValueMap.class);
                    final String repPrincipalName = props.get("rep:principalName", String.class);

                    if (principalNames == null
                            || principalNames.isEmpty()
                            || principalNames.contains(repPrincipalName)) {

                        resources.add(repPolicy);

                        log.debug("Included by principal [ {} ]", repPolicy.getPath());
                        break;
                    }
                }
            }
        }

        for (final Resource resource : resources) {
            pathFilterSets.add(new PathFilterSet(resource.getPath()));
        }

        log.debug("Found {} matching rep:policy resources.", pathFilterSets.size());
        return pathFilterSets;
    }

Once all of the values required for the package are defined, the custom method doPackaging is invoked. This method is defined in the AbstractPackagerServlet class. The following values are passed to this method:

  • SlingHttpServletRequest request
  • SlingHttpServletResponse response
  • boolean preview
  • ValueMap properties
  • List<PathFilterSet> packageResources

These values are used to dynamically create the AEM Package. The ValueMap argument contains the properties such as the name of the package. In the doPackaging method, a Map object is created by this line of code:

final Map packageDefinitionProperties = new HashMap();

This Map object is used to store package defination values used to build the AEM Package. This Map object is populated with values by using the following code.

 

 packageDefinitionProperties.put(
                JcrPackageDefinition.PN_DESCRIPTION,
                properties.get(PACKAGE_DESCRIPTION, getDefaultPackageDescription()));

Notice how the Map object's put method is called and two values are passed: 

  • JcrPackageDefinition static constant value. See Interface JcrPackageDefinition.
  • The value of the packageDescription property defined in the Packager value dialog. 

The following code that builds the AEM package and gets back a JcrPackage object. 

 

 

// Create JCR Package; Defaults should always be passed in via Request Parameters, but just in case
            final JcrPackage jcrPackage = getPackageHelper().createPackageFromPathFilterSets(packageResources,
                    request.getResourceResolver().adaptTo(Session.class),
                    properties.get(PACKAGE_GROUP_NAME, getDefaultPackageGroupName()),
                    properties.get(PACKAGE_NAME, getDefaultPackageName()),
                    properties.get(PACKAGE_VERSION, DEFAULT_PACKAGE_VERSION),
                    PackageHelper.ConflictResolution.valueOf(properties.get(CONFLICT_RESOLUTION,
                            PackageHelper.ConflictResolution.IncrementVersion.toString())),
                    packageDefinitionProperties
            );

This code creates a PackageHelper object that is defined in the com.adobe.acs.commons.packaging package. This class exposes a method named createPackageFromPathFilterSets that creates an AEM package. The following Java code represents this method, which is defined in the PackageHelperImpl class. 

  public JcrPackage createPackageFromPathFilterSets(final Collection<PathFilterSet> pathFilterSets,
                                                      final Session session,
                                                      final String groupName, final String name, String version,
                                                      final ConflictResolution conflictResolution,
                                                      final Map<String, String> packageDefinitionProperties)
            throws IOException, RepositoryException {

        final JcrPackageManager jcrPackageManager = packaging.getPackageManager(session);

        if (ConflictResolution.Replace.equals(conflictResolution)) {
            this.removePackage(jcrPackageManager, groupName, name, version);
        } else if (ConflictResolution.IncrementVersion.equals(conflictResolution)) {
            version = this.getNextVersion(jcrPackageManager, groupName, name, version).toString();
        }

        final JcrPackage jcrPackage = jcrPackageManager.create(groupName, name, version);
        final JcrPackageDefinition jcrPackageDefinition = jcrPackage.getDefinition();
        final DefaultWorkspaceFilter workspaceFilter = new DefaultWorkspaceFilter();

        for (final PathFilterSet pathFilterSet : pathFilterSets) {
            workspaceFilter.add(pathFilterSet);
        }

        jcrPackageDefinition.setFilter(workspaceFilter, true);

        for (final Map.Entry<String, String> entry : packageDefinitionProperties.entrySet()) {
            jcrPackageDefinition.set(entry.getKey(), entry.getValue(), false);
        }

        session.save();

        return jcrPackage;
    }

The first task that this method performs is to create a JcrPackageManager object (shown at line 8 in the previous code example). This object is part of the AEM PackageManager API. For information, see Interface JcrPackageManager.

 

Note:

In this example, notice that the packaging.getPackageManager method returns a JcrPackageManager object. The instance is an object of type Interface Packaging. Furthermore, dependency injection is used to create the object: 

 @Reference   

private Packaging packaging;

 

The createPackageFromPathFilterSets method creates a package using this method: 

 final JcrPackage jcrPackage = jcrPackageManager.create(groupName, name, version);

Some additional logic is performed and this method returns the JcrPackage instance.

The remaining part of the doPacking method performs tasks like thumbnail tasks. The following code shows the remaining part of the doPackaging method.  

  String thumbnailPath = getPackageThumbnailPath();

            if (thumbnailPath != null) {
                // Add thumbnail to the package definition
                getPackageHelper().addThumbnail(jcrPackage,
                        request.getResourceResolver().getResource(thumbnailPath));
            }

            log.debug("Successfully created JCR package");
            response.getWriter().print(
                    getPackageHelper().getSuccessJSON(jcrPackage));

Invoke the Servlet from the client AEM page

You can invoke the servlet that builds the AEM Package by creating an AEM Page that is based on the template located at /apps/acs-commons/templates/utilities/acl-packager. 

client
An AEM page based on the ACL Packager template and invokes that servlet that builds AEM packages

To create a page based on the ACL Packager template, perform these tasks.

1. Navigate to the Classic UI Tools Console (from the Touch UI, this is Tools, Operations,Configuration).

2. Under acs-commons, select the Content Packagers folder. 

3. Create a new Page based on ACL Packager.

 

 

Template
Creating a page based on a specific template

4. Use the Edit dialog to configure the package rules and configuration.

button
The Edit dialog button

5. Enter the dialog values, as shown in the following illustration (these are the values that are passed to the Sling Servlet). 

dialogfill
Dialog values

6. Click the Create Package button. 

CreatePackage
Create Package button

7. If the package is successfully built, you will see the following message on the page.

Success
An AEM Page is successfully built

Understanding the acl-packager template

The template on which the page is based is located at:

/apps/acs-commons/templates/utilities/acl-packager

The sling resource type is acs-commons/components/utilities/packager/authorizable-packager. This means the script for this page is located under the authorizable-packager node. The following script represents the logic for this page. 

 

<%@include file="/libs/foundation/global.jsp"%><%
%><%@page session="false" contentType="text/html" pageEncoding="utf-8" %><%

    /* Package Definition */
    final String[] principalNames = properties.get("principalNames", new String[]{});
    final String[] includePatterns = properties.get("includePatterns", new String[]{});

    final String packageName = properties.get("packageName", "acls");
    final String packageGroupName = properties.get("packageGroupName", "ACLs");
    final String packageVersion = properties.get("packageVersion", "1.0.0");
    final String packageDescription = properties.get("packageDescription", "ACL Package initially defined by a ACS AEM Commons - ACL Packager configuration.");

    final String packageACLHandling = properties.get("packageACLHandling", "Overwrite");
    final String conflictResolution = properties.get("conflictResolution", "IncrementVersion");

    final boolean includePrincipals = properties.get("includePrincipals", false);
    final boolean includeConfiguration = properties.get("includeConfiguration", false);
%>

<h3>Package definition</h3>
<ul>
    <li>Package name: <%= xssAPI.encodeForHTML(packageName) %></li>
    <li>Package group: <%= xssAPI.encodeForHTML(packageGroupName) %></li>
    <li>Package version: <%= xssAPI.encodeForHTML(packageVersion) %></li>
    <li>Package description: <%= xssAPI.encodeForHTML(packageDescription) %></li>
    <li>Package ACL handling: <%= xssAPI.encodeForHTML(packageACLHandling) %></li>
    <li>Conflict resolution: <%= xssAPI.encodeForHTML(conflictResolution) %></li>
    <li>Include principals: <%= includePrincipals %></li>
    <li>Include ACL packager page: <%= includeConfiguration %></li>
</ul>

<h3>Targeted principals</h3>
<ul>
    <% if(principalNames.length == 0) { %>
    <li class="not-set">All principals</li>
    <% } %>
    <% for(final String principalName : principalNames) { %>
        <li><%= xssAPI.encodeForHTML(principalName) %></li>
    <% } %>
</ul>

<h3>Include patterns</h3>
<ul>
    <% if(includePatterns.length == 0) { %>
    <li class="not-set">All paths</li>
    <% } %>
    <% for(final String includePattern : includePatterns) { %>
    <li><%= xssAPI.encodeForHTML(includePattern) %></li>
    <% } %>
</ul>

<%-- Common Form (Preview / Create Package) used for submittins Packager requests --%>
<%-- Requires this configuration component have a sling:resourceSuperType of the ACS AEM Commons Packager --%>
<cq:include script="partials/form.jsp"/>

The form part of the page is defined in the partials/form.jsp. The following script represents the form.jsp.

<%@include file="/libs/foundation/global.jsp"%><%
%><%@page session="false" contentType="text/html" pageEncoding="utf-8" %><%
    final String actionURI = resourceResolver.map(slingRequest, currentPage.getContentResource().getPath() + ".package.json");
%>

<hr/>

<form id="packager-form" method="post" action="<%= xssAPI.getValidHref(actionURI) %>">
    <input class="button" name="preview" type="submit" value="Preview"/>
    <input class="button" name="create" type="submit" value="Create Package"/>
</form>

In this script, notice this line of code:

final String actionURI = resourceResolver.map(slingRequest, currentPage.getContentResource().getPath() + ".package.json");

This is defining the URL that is used to post data to the ACLPackagerServletImpl servlet. Notice that it maps to the @SlingServlet annotation, That is, it maps to the resourceType, the extension, and selector defined in this annotation. 

 

@SlingServlet(
        methods = { "POST" },
        resourceTypes = { "acs-commons/components/utilities/packager/acl-packager" },
        selectors = { "package" },
        extensions = { "json" }
)

This is how this JSP invokes the Sling Servlet defined in the ACLPackagerServletImpl class (discussed earlier in this article). 

Note:

It is important that you understand the relationship between form.jsp and the sling servlet discussed in this article. This is how you invoke the servlet  that is registered using resource types. If you want to know how to post data to an AEM servlet that is registered using paths, see
Submitting Experience Manager form data to Java Sling Servlets.

The applicaton logic that handles a successful post operation is defined in the /apps/acs-commons/components/utilities/packager/clientlibs/js/packager.js file. WHen a successful post operation occurs, the web page is updated using this code:

 

$('html, body').animate({
                scrollTop: $('.notifications').offset().top - 20
            }, 500);
        }, 'json');

The following represents this JS file. 

$(function() {
    $('body').on('click', '#packager-form input[type=submit]', function(e) {

        var $this = $(this),
            $form = $this.closest('form'),
            json,
            i;

        $('.notification').fadeOut();

        $.post($form.attr('action'), {'preview': $this.attr('name') === 'preview' }, function(data) {
            $('.notification').hide();

            if(data.status === 'preview') {
                /* Preview */

                $('.notification.preview .filters').html('');
                if(!data.filterSets) {
                    $('.notification.preview .filters').append('<li>No matching resources found.</li>');
                } else {
                    for(i = 0; i < data.filterSets.length; i++) {
                        $('.notification.preview .filters').append('<li>' + data.filterSets[i].rootPath + '</li>');
                    }
                }

                $('.notification.preview').fadeIn();
            } else if(data.status === 'success') {
                /* Success */

                $('.notification.success .package-path').text(data.path);
                $('.notification.success .package-manager-link').attr('href', '/crx/packmgr/index.jsp#' + data.path);

                $('.notification.success .filters').html('');

                if(!data.filterSets) {
                    $('.notification.preview .filters').append('<li>No matching resources found.</li>');
                } else {
                    for(i = 0; i < data.filterSets.length; i++) {
                        $('.notification.success .filters').append('<li>' + data.filterSets[i].rootPath + '</li>');
                    }
                }

                $('.notification.success').fadeIn();
            } else {
                /* Error */
                $('.notification.error .msg').text(data.msg || '');
                $('.notification.error').fadeIn();
            }

            $('html, body').animate({
                scrollTop: $('.notifications').offset().top - 20
            }, 500);
        }, 'json');

        e.preventDefault();
        //return false;
    });
});

See also

Congratulations, you have just created an AEM app that builds packages. 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