Wednesday, May 1, 2013

Preview a Historical Version of a Node in JCR

We ran into a issue of exporting a particular version of page from CQ. So the version of a page is stored as a jcr:frozenNode, which is not a literal copy of jcr:content. Instead, it did a few things.
  
It removes a few version-related properties:
  • jcr:baseVersion
  • jcr:predecessors
  • jcr:versionHistory
  • jcr:isCheckedOut 
It renames a few system-level properties by adding "frozen" prefix
  • jcr:primaryType => jcr:frozenPrimaryType
  • jcr:uuid => jcr:frozenUuid
  • jcr:mixinTypes => jcr:frozenMixinTypes 
And finally it adds a few properties:
  • cq:childOrder
  • cq:name
  • cq:parentPath
  • cq:siblingOrder
  • jcr:primaryType (of the version node)
  • jcr:uuid (of the version node)
Even though the pattern is very straightforward, we don't want to do the dirty job by ourselves. The first thing I thought is to use CQ's API and PageManager was found. It provides a restore method which did the job. However, it restores to the current page immediately even though you haven't called save on the underlying Session object of the ResourceResolver.

I thought it's a defect of CQ API, since its JCR implementation CRX doesn't transaction. Maybe it calls save internally. Then I decided to dig deep into JCR's API, and VersionManager is found. It also provides a restore method which I thought will do the job, but guess what? On its description, it says: If successful, the change is dispatched immediately; there is no need to call save. That's a much bigger issue, meaning there is no easy way just to preview the content of a historical version without affect the current node. That sucks.

However, I believe there must be a workaround. You can preview a historical version of a page through it's build-in Timewarp feature, although this code is not public. So the next thing I thought is to copy my current node to a temporary location and then restore to it Unfortunately, it didn't work because when a node is copied, the new node will have a different identifier, and therefore links to a different version history. JCR enforce the 1-to-1 mapping between each versionable node to each version history node in a given workspace. Therefore, you can't restore the version of the original node to the copied node since that version doesn't belong to it.

I also tried to search online, but couldn't find anything. At the end, I ran out of ideas and gave up. Luckily, I happened to find something valuable a few days later when I was skimming over the JCR spec.

Cloning Nodes Across Workspaces:
The clone method clones both referenceable and non-referenceable nodes and preserves the identifier of every node in the source subgraph. 

If the cloned node shares the same identifier, it will share the same version history! So I tried my luck of creating a temporary workspace, cloning the jcr:content node to that temporary workspace, and restoring to the cloned node. It works!

Firstly, create a temporary workspace.
Session session = repository.loginAdministrative(null);
if (!ArrayUtils.contains(session.getWorkspace().getAccessibleWorkspaceNames(), "CRX.temp")) {
    Workspace workspace = session.getWorkspace();
    workspace.createWorkspace("CRX.temp");
}
Then, just clone the node you want to preview to the temporary workspace and restore to it.
// This is the default Session which uses the default workspace in CQ. 
Session session;
Workspace workspace = session.getWorkspace();

// Grab the session and workspace for the temporary workspace
Session tmpSession = repository.loginAdministrative("CRX.temp");
Workspace tmpWorkspace = tmpSession.getWorkspace();

tmpWorkspace.clone(workspace.getName(), "/path/of/current/node", "/clone", false);
tmpWorkspace.getVersionManager().restore("/clone", "1.2", false);

Node jcrContent = tmpSession.getNode(tmpContentPath);

// Do whatever you want here.