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;

@Component
@Service
@Properties({
        @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());
            workflowSession.terminateWorkflow(workItem.getWorkflow());
            return;
        }
        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);
            workflowSession.terminateWorkflow(workItem.getWorkflow());
            return;
        }
        
        //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
            contentNode.setProperty(NameConstants.PN_TEMPLATE,
                    "/apps/geometrixx/templates/contentpage");                          //cq:template
            contentNode.setProperty(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY,
                    "geometrixx/components/contentpage");                               //sling:resourceType
            
            Node parsysNode = contentNode.addNode("par");
            parsysNode.setProperty(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY,
                    "foundation/components/parsys");
            
            Node textNode = parsysNode.addNode("text");
            textNode.setProperty(JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY,
                    "foundation/components/text");
            textNode.setProperty("text", lipsum);
            textNode.setProperty("textIsRich", true);
            
            workflowSession.getSession().save();
        }
        catch (RepositoryException e) {
            log.error(e.toString(), e);
            workflowSession.terminateWorkflow(workItem.getWorkflow());
            return;
        }
    }
}

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.

26 April 2012

Q and A

I invite you to participate in the development of a Q&A site dedicated to the History of Computers and Computing. The site will be part of the successful Stack Exchange network, of which Stack Overflow is the flagship, and is "for historians, enthusiasts and those who have personally experienced the evolution of computing."

One of the features of a Stack Exchange site that makes it successful is the liberal awarding of brownie points to users who constructively participate in the site. Jeff Atwood and his team have figured out the alchemy that makes a community-managed site work.

At this point, the proposed site is in the stage of soliciting committers. Follow the link below and join us!

Stack Exchange Q&A site proposal: History of Computers and Computing

25 April 2012

!JOB ARSRCHAA,RPT/*.IVL;OUTCLASS=LP,1

Today's mystery is a dusty deck submitted by sachin to Stack Overflow. He's charged with converting this old report code to SQL, but there's one problem: he can't even identify the language that the old program is coded in! Gilbert Le Blanc surmises that the JCL identifies the host computer as a Systems Engineering Laboratories/Gould/Encore machine, but there the trail ends.

19 April 2012

Doomed

A really unpleasant backwards-compatibility feature of the Windows shell that I wasn't aware of.
It wasn’t until Windows NT 3.5.1 (and then Windows 95 later) that long file names were supported. Prior to this, there were a lot of limitations on what characters could be part of a filename or directory, one of those being a space.

In fact, any space in a shell command execution was seen to be an argument. This made sense at the time so you could issue a command like this:

C:\DOOM\doom.exe -episode 3

That, of course, would start Doom at episode 3. However, when Microsoft switched to Long File Names, it still had to support this type of invocation. So, the way the windows cmd.exe shell works is simple. You pass it a string like this:

C:\Program Files\id Software\Doom\Doom.exe -nomusic

And it will try to execute “C:\Program” as a file, passing it “Files\id Software\Doom\Doom.exe -nomusic” as argument to that executable. Of course, this program doesn’t exist, so it will then try to execute “C:\Program Files\id”, passing it “Software\Doom\Doom.exe -nomusic” as argument. If this doesn’t exist, it will try to execute “C:\Program Files\id Software\Doom\Doom.exe” passing in “-nomusic” as an argument. It would continue this way until a program existed and started, or until the path was depleted and no program was to be found.

11 April 2012

Work in progress

Audrey Watters accounts for the lack of success, or perceived lack of success, of the OLPC program and related education efforts. Julian Cristia et al. have recently released a study for the Inter-American Development Bank of the scheme's effectiveness in Peru. Watters:
That last (missing) piece -- training for teachers -- has long been something that gets overlooked when it comes to ed-tech initiatives no matter the location, Peru or the U.S. It is almost as if we believe we can simply parachute technology in to a classroom and expect everyone to just pick it up, understand it, use it, hack it, and prosper.
The Code Project