AEM Package Testing With Oakpal‑maven‑plugin
Editor's Note: Mark presented "OakPAL Interactive" and won at the 2019 AEM Rock Star competition at Adobe Summit.
TL;DR: Paste the following into a content-package
pom.xml and get started with oakpal-maven-plugin.
<plugin>
<groupId>net.adamcin.oakpal</groupId>
<artifactId>oakpal-maven-plugin</artifactId>
<version>1.2.0</version>
<executions>
<execution>
<goals>
<goal>scan</goal>
</goals>
</execution>
</executions>
</plugin>
If you develop code for AEM, then you probably build and install content-packages from source using Maven. You might also be familiar with the fact that the process of installing such a package on AEM can be prone to both non-deterministic failures and less-than-complete “successes.”
What you may not know is that, other than the traditional culprits of lack of disk space or permissions, or the occasional Package Manager bundle restart, the vast majority of package installation failures are caused by a capricious gremlin that lives in the packages themselves, and that gremlin’s name is “DocView.”
The Insidiousness of DocView Errors
FileVault uses the JCR Document View (DocView) XML serialization format to import arbitrary trees of nodes from packages into the repository.
The DocView format consists of mapping XML element names to JCR node names, and XML attributes to JCR properties. The relationship between the two models is so easy to grasp that the small impedance mismatch that actually exists can lead to mistakes that are very difficult to detect in an IDE or during code review.
Some common mistakes are:
- Unescaped ampersands in attributes (very common when editing these files by hand)
- .content.xml files mangled by blind search-and-replace operations
- Use of undeclared or unregistered JCR namespace prefixes in attribute values
- Missing or violated node type definitions
- Unspecified parent node types, which default to the hilariously restrictive
nt:folder
Thanks to hybrid nature of the format, only part of the represented structure are visible to a standalone XML parser. JCR node types and property types are just opaque strings in the XML attribute values, so a non-JCR aware XML parser or schema validator will simply ignore them. In order to catch all the possible errors that can arise when installing a DocView file, you must actually attempt to install the package into a JCR-compliant repository and watch for errors.
But watching for errors is a challenge as well. When individual DocView files fail to import, the errors raised are often buried in verbose log streams or are suppressed completely during development. Even more frustrating is that these errors do not often fail the package installation, and instead the importer merely spits out an “E” for the path and then continues undaunted, returning a 200 response code while only dribbling out an underwhelming log message at the end:
saving approx 4832842 nodes...
Package imported (with errors, check logs!)
Did I mention I don’t always fail?
Attack the DocView Gremlin at the Source
To fill the AEM package accepting testing gap, I’ve created a project called OakPAL – The Oak Package Acceptance Library that exposes a simple java API for simulating content package installation without incurring the overhead of launching an OSGi runtime or connecting to an HTTP port. Built around this core API, the oakpal-maven-plugin hooks into your maven lifecycle during the integration-test
phase to identify any possible issues with the package itself that would prevent installation downstream.
The first step is to add the plugin to your content-package
pom.
<plugin>
<groupId>net.adamcin.oakpal</groupId>
<artifactId>oakpal-maven-plugin</artifactId>
<version>1.2.0</version>
<executions>
<execution>
<goals>
<goal>scan</goal>
</goals>
</execution>
</executions>
</plugin>
At this point, your module will attempt to install the package artifact into a vanilla Oak repository–the keyword being “vanilla”.
A happy installation looks like this:
[INFO] --- oakpal-maven-plugin:1.2.0:scan (default) @ my-project.ui.apps ---
[INFO] Found a new index node [reference]. Reindexing is requested
[INFO] Reindexing will be performed for following indexes: [/oak:index/uuid, /oak:index/reference, /oak:index/nodetype]
[INFO] Indexing report
- /oak:index/nodetype*(1257)
[INFO] Reindexing will be performed for following indexes: [/oak:index/principalName, /oak:index/authorizableId, /oak:index/acPrincipalName, /oak:index/repMembers]
[INFO] Indexing report
- /oak:index/principalName*(2)
- /oak:index/authorizableId*(2)
If all you have are folders, jar files, and nt:unstructured
nodes, no problem. But more likely than not, you are developing a package containing Sling resources, or AEM templates and components, which means you are probably dependent on the namespaces and nodetypes that are installed only with the product, like sling:OsgiConfig
and cq:Component
, for example.
Without these nodetypes you will probably see FileVault logging similar to this:
[ERROR] Error during processing of /apps/my-project/components/content/colctrl: javax.jcr.nodetype.NoSuchNodeTypeException: Node type cq:Component does not exist
[ERROR] E /apps/my-project/components/content/colctrl (javax.jcr.nodetype.NoSuchNodeTypeException: Node type cq:Component does not exist)
[ERROR] E /apps/my-project/components/content/colctrl/clientlib (java.lang.IllegalStateException: Parent node not found.)
[ERROR] E /apps/my-project/components/content/colctrl/clientlib/css.txt (java.lang.IllegalStateException: Parent node not found.)
[ERROR] E /apps/my-project/components/content/colctrl/clientlib/style.css (java.lang.IllegalStateException: Parent node not found.)
Followed by associated OakPAL Violation Reports:
[INFO] OakPAL Reporter: jar:file:/Users/madamcin/.m2/repository/net/adamcin/oakpal/oakpal-core/1.2.0/oakpal-core-1.2.0.jar!/net/adamcin/oakpal/core/DefaultErrorListener.class
[ERROR] +- <MAJOR> /apps/my-project/components/content/colctrl - Importer error: javax.jcr.nodetype.NoSuchNodeTypeException "Node type cq:Component does not exist"
[ERROR] +- <MAJOR> /apps/my-project/components/content/colctrl/clientlib - Importer error: java.lang.IllegalStateException "Parent node not found."
[ERROR] +- <MAJOR> /apps/my-project/components/content/colctrl/clientlib/css.txt - Importer error: java.lang.IllegalStateException "Parent node not found."
[ERROR] +- <MAJOR> /apps/my-project/components/content/colctrl/clientlib/style.css - Importer error: java.lang.IllegalStateException "Parent node not found."
To install the AEM platform node types, you can export them from CRX/de lite.
Export your AEM platform nodetypes
To properly prepare the scan for your code package, you might first need to export the Compact NodeType Definition (CND) from your installed version of AEM and make it available to the plugin.
For a developer, it is as simple as visiting crx/de lite on a representative installation, such as a properly patched local quickstart server.
Navigate in the toolbar to Tools > Export Node Type:
You will see the generated CND content rendered directly.
<'sling'='http://sling.apache.org/jcr/sling/1.0'>
<'nt'='http://www.jcp.org/jcr/nt/1.0'>
<'cq'='http://www.day.com/jcr/cq/1.0'>
<'oak'='http://jackrabbit.apache.org/oak/ns/1.0'>
<'jcr'='http://www.jcp.org/jcr/1.0'>
<'mix'='http://www.jcp.org/jcr/mix/1.0'>
<'granite'='http://www.adobe.com/jcr/granite/1.0'>
<'rep'='internal'>
<'xmp'='http://ns.adobe.com/xap/1.0/'>
<'social'='http://www.adobe.com/social/1.0'>
<'dam'='http://www.day.com/dam/1.0'>
<'oauth'='http://oauth.net/'>
<'rdf'='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
<'vlt'='http://www.day.com/jcr/vault/1.0'>
<'slingevent'='http://sling.apache.org/jcr/event/1.0'>
<'fd'='http://www.adobe.com/aemfd/fd/1.0'>
[sling:OrderedFolder] > sling:Folder
orderable
+ * (nt:base) = sling:OrderedFolder version
[cq:OwnerTaggable] > cq:Taggable
mixin
[oak:Unstructured]
- * (undefined) multiple
- * (undefined)
+ * (nt:base) = oak:Unstructured version
...
Save the output as a file under src/test/resources
in your ui.apps module and add the
parameter to your oakpal-maven-plugin configuration with the path to the file relative to src/test/resources
.
<plugin>
<groupId>net.adamcin.oakpal</groupId>
<artifactId>oakpal-maven-plugin</artifactId>
<version>1.2.0</version>
<configuration>
<cndNames>
<cndName>[your-cnd-filename]</cndName>
</cndNames>
</configuration>
<executions>
<execution>
<goals>
<goal>scan</goal>
</goals>
</execution>
</executions>
</plugin>
Run mvn install
again and hope for success
Advanced Case Study: Dependency on ACS AEM Commons
Things are never as simple as they seem in the AEM world, and this plugin is proud to follow in that tradition. You may have already started asking questions like, “What if my package depends on another package being installed first?”, and “What if the exported CND doesn’t install all the namespaces that my package depends on?”. I’ll answer those questions by demonstrating how to handle the common situation where your code package has a dependency on ACS AEM Commons.
To successfully simulate installation into a repository where ACS Commons has been installed we will need to:
- Register the
crx
namespace - Register the
crx:replicate
privilege - Pre-install the
acs-aem-commons-content
package
Behold the final dazzling plugin configuration:
<plugin>
<groupId>net.adamcin.oakpal</groupId>
<artifactId>oakpal-maven-plugin</artifactId>
<version>1.2.0</version>
<configuration>
<cndNames>
<cndName>[your-cnd-filename]</cndName>
</cndNames>
<jcrNamespaces>
<jcrNamespace>
<prefix>crx</prefix>
<uri>http://www.day.com/crx/1.0</uri>
</jcrNamespace>
</jcrNamespaces>
<jcrPrivileges>
<jcrPrivilege>crx:replicate</jcrPrivilege>
</jcrPrivileges>
<preInstallArtifacts>
<preInstallArtifact>
<groupId>com.adobe.acs</groupId>
<artifactId>acs-aem-commons-content</artifactId>
<version>4.0.0</version>
<type>zip</type>
</preInstallArtifact>
</preInstallArtifacts>
</configuration>
<executions>
<execution>
<goals>
<goal>scan</goal>
</goals>
</execution>
</executions>
</plugin>
I encourage you to read the oakpal-maven-plugin:scan
reference page to see the other available options.