30 April 2012

Fun with CQ5: 1

The code examples provided by Adobe for CQ5 developers are fine so far as they go. But I found myself exploring the territory without a map when I started prototyping code to perform a bulk import (from a legacy CMS, in my case). So I put together this toy class; it inserts a content page and text (as a parsys) at an arbitrary position in the content hierarchy.
I wrapped this functionality into a workflow process step, but it ought to work perfectly well as part of a command-line program, too. The advantage of making it a process step is that you get a session established for you, and the navigation to the insert point has been taken care of. (I haven't yet explored developing for CQ5 with standalone programs).
I created a workflow model that uses this process step, then I created an instance of the model and ran it, specifying the English > Products > Triangle page from the Geometrixx web site as the payload.
Here's the code, sanitized to remove client names and a little condensed. I'm using version 5.4.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.jcr.Node;
import javax.jcr.RepositoryException;

import org.apache.sling.jcr.resource.JcrResourceConstants;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;

import com.day.cq.wcm.api.NameConstants;

        @Property(name = Constants.SERVICE_DESCRIPTION,
            value = "Makes a new tree of nodes, subordinate to the payload node, from the content of a file."),
        @Property(name = Constants.SERVICE_VENDOR, value = "Siteworx"),
        @Property(name = "process.label", value = "Make new nodes from file")})
public class PageNodesFromFile implements WorkflowProcess {

    private static final Logger log = LoggerFactory.getLogger(PageNodesFromFile.class);
    private static final String TYPE_JCR_PATH = "JCR_PATH";
* * * 
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
            throws WorkflowException {

        //get the payload
        WorkflowData workflowData = workItem.getWorkflowData();
        if (!workflowData.getPayloadType().equals(TYPE_JCR_PATH)) {
            log.warn("unusable workflow payload type: " + workflowData.getPayloadType());
        String payloadString = workflowData.getPayload().toString();

        //get the file contents
        String lipsum = null;
        try {
            BufferedReader is = new BufferedReader(new FileReader("e:\\Sandbox\\CQ5\\content.html"));
            lipsum = readerToString(is);
        catch (IOException e) {
            log.error(e.toString(), e);
        //set up some node info
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("d-MMM-yyyy-HH-mm-ss");
        String newRootNodeName = "demo-page-" + simpleDateFormat.format(new Date());
        SimpleDateFormat simpleDateFormatSpaces = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
        String newRootNodeTitle = "Demo page: " + simpleDateFormatSpaces.format(new Date());
        //insert the nodes
        try {
            Node parentNode = (Node) workflowSession.getSession().getItem(payloadString);
            Node pageNode = parentNode.addNode(newRootNodeName);
            pageNode.setPrimaryType(NameConstants.NT_PAGE);                             //cq:Page
            Node contentNode = pageNode.addNode(Node.JCR_CONTENT);                      //jcr:content
            contentNode.setPrimaryType("cq:PageContent");                               //or use MigrationConstants.TYPE_CQ_PAGE_CONTENT
                                                                                        //from com.day.cq.compat.migration
            contentNode.setProperty(javax.jcr.Property.JCR_TITLE, newRootNodeTitle);    //jcr:title
                    "/apps/geometrixx/templates/contentpage");                          //cq:template
                    "geometrixx/components/contentpage");                               //sling:resourceType
            Node parsysNode = contentNode.addNode("par");
            Node textNode = parsysNode.addNode("text");
            textNode.setProperty("text", lipsum);
            textNode.setProperty("textIsRich", true);
        catch (RepositoryException e) {
            log.error(e.toString(), e);

Here's the comp text in e:\\Sandbox\\CQ5\\content.html:
Veggies sunt bona vobis, proinde vos postulo esse magis earthnut pea catsear cress sea lettuce quandong scallion rock melon seakale jícama komatsuna onion.
Bush tomato garbanzo beetroot caulie plantain sorrel swiss chard summer purslane celtuce salad seakale rutabaga radicchio lettuce spring onion groundnut soko peanut. Tigernut bitterleaf bush tomato celery corn garbanzo bamboo shoot cauliflower komatsuna cress sweet pepper mustard squash. Celtuce parsley kakadu plum coriander peanut garlic radish water chestnut tomatillo yarrow parsnip.
Squash endive collard greens tigernut bamboo shoot okra melon turnip. Rock melon amaranth ricebean pea chickpea nori bitterleaf spring onion bush tomato aubergine beetroot lotus root earthnut pea artichoke eggplant collard greens chard water spinach. Prairie turnip napa cabbage lettuce bush tomato garlic chickweed wattle seed potato lotus root pea sprouts leek kakadu plum. Radish leek green bean epazote water chestnut bamboo shoot celtuce taro tomatillo horseradish lettuce spring onion. Mustard taro prairie turnip horseradish wattle seed kohlrabi rock melon yarrow broccoli rabe fennel spinach celery collard greens gourd turnip.
Here's what the Websites window looks like after execution: cq5-demo-websites
Here's what the corresponding node looks like in CRXDE Lite: cq5-demo-crxde-lite
And here's what the page looks like in the content finder: cq5-demo-rendered
Some notes about the demonstrator code:
  • I like to use lorem ipsum-style text (I got this text from Veggie Ipsum) when I'm testing. It's always clear that you're using test data rather than a copy of something live. And in the unlikely event that your test data leaks into production, it's a lot less embarrassing to see "lorem ipsum" than "asdf jkl; asdf jkl;" or "yo mama" in 16-point type, in my opinion.
  • I incorporated a timestamp into the name and title of the content page to be inserted. That way, you can run many code and test cycles without cleaning up your repository, and you know which test was the most recently run. Added bonus: no duplicate file names, no ambiguity.
  • Adobe and Day have been inconsistent about providing constants for property values, node types, and suchlike. I used the constants that I could find, and used literal strings elsewhere.
  • I did not fill in properties like the last-modified date. In code for production I would do so.
  • I found myself confused by Node.setPrimaryType() and Node.getPrimaryNodeType(). The two methods are only rough complements; the setter takes a string but the getter returns a NodeType with various info inside it.

No comments: