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>0.3.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 is 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!)
I don't always fail, but when I do, I also list all the things I did right
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>0.3.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:0.3.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/0.2.0/oakpal-core-0.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:

CRX/de lite: 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 <cndFiles>/<cndFile> parameter to your oakpal-maven-plugin configuration with the path to the file.

<plugin>
  <groupId>net.adamcin.oakpal</groupId>
  <artifactId>oakpal-maven-plugin</artifactId>
  <version>0.3.0</version>
  <configuration>
    <cndFiles>
      <cndFile>src/test/resources/[your-cnd-filename]</cndFile>
    </cndFiles>
  </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:

  1. Register the crx namespace
  2. Register the crx:replicate privilege
  3. Force the creation of the /home/users/system folder as a rep:AuthorizableFolder
  4. 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>0.3.0</version>
  <configuration>
    <cndFiles>
      <cndFile>src/test/resources/[your-cnd-filename]</cndFile>
    </cndFiles>
    <jcrNamespaces>
      <jcrNamespace>
        <prefix>crx</prefix>
        <uri>http://www.day.com/crx/1.0</uri>
      </jcrNamespace>
    </jcrNamespaces>
    <jcrPrivileges>
      <jcrPrivilege>crx:replicate</jcrPrivilege>
    </jcrPrivileges>
    <forcedRoots>
      <forcedRoot>
        <path>/home/users/system</path>
        <primaryType>rep:AuthorizableFolder</primaryType>
        <mixinTypes>
          <mixinType>rep:AccessControllable</mixinType>
        </mixinTypes>
      </forcedRoot>
    </forcedRoots>
    <preInstallArtifacts>
      <preInstallArtifact>
        <groupId>com.adobe.acs</groupId>
        <artifactId>acs-aem-commons-content</artifactId>
        <version>3.9.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.

Is that all?

Stay tuned for Part 2, in which I will demonstrate how to use JavaScript violation reporters to enforce style and quality checks on your DocView nodes. Here is a sneak peek at writing an OakPAL scriptReporter.