Archive for August, 2009

When you generate sitepublisher pages in the out of the box workflow, it runs as a CGI script invoking /iw-cc/Workflow/Webtask.do. This requires the user to start the input task in certain circumstances which may not always be desirable. In those cases, we want to run the page generation as a URLExternalTask, not as a CGI script.

how does it work?

In the Teamsite web application, *.do requests are intercepted by struts and so is our /iw-cc/Workflow/Webtask.do request. No exception… A task variable with a name of TaskBeanId is passed to the task and struts will instantiate an object of the corresponding class by resolving the bean entry from the file httpd/webapps/content_center/WEB-INF/conf/livesite/resources/workflow-resource-config.xml.

The TaskBeanId in question in the original task is workflow.task.PageConversionTask and, looking in the file workflow-resource-config.xml, we can translate it to com.interwoven.livesite.workflow.web.task.PageConversionWebTask.

<bean id="workflow.task.PageConversionTask"
    class="com.interwoven.livesite.workflow.web.task.PageConversionWebTask">
 <property name="viewPath" value="/livesite/Workflow/PageConversionWebTask.jsp"/>
 <property name="suffixVariableName" value="Suffix"/>
 <property name="processorVariableName" value="Processor"/>
 <property name="destinationDirectoryVariableName" value="DestinationDirectory"/>
 <property name="filterPatternVariableName" value="FilterPatterns"/>
 <!-- if you want a custom delimiter for the filter patterns, default is comma -->
 <property name="filterPatternDelimiterVariableName" value="FilterPatternDelimiter"/>
 </bean>

When an object of that class is instantiated, the other properties associated with the bean are set with the values given. These will allow the object to query the task and find the task variable with a name of Suffix, Processor, DestinationDirectory,FilterPatterns and FilterPatternDelimiter.

Creating a URLEXternalTask

Inside workflowModeler, add a URL Task to the canvas of the workflow model. set the predecessor by linking to the task from another task or the start event icon. Set the name of the task to be “Generate HTML”. This name can be whatever you want but it is quite significant elsewhere as we will see later. Set the URI to be http://localhost/iw-cc/urlexternaltask.

In the Task Variables, we will specify the class to execute and no, we will not use the class com.interwoven.livesite.workflow.web.task.PageConversionWebTask because that class expects some JSP to forward the request to. No, instead we shall look at the other bean defined by the following XML:

<bean id="workflow.task.HeadlessPageConversionTask"
    class="com.interwoven.livesite.workflow.task.PageConversionTask">
 <property name="suffixVariableName" value="Suffix"/>
 <property name="processorVariableName" value="Processor"/>
 <property name="destinationDirectoryVariableName" value="DestinationDirectory"/>
 <property name="servicePath" value="/iw-cc/livesite/PageConversionServlet"/>
 <property name="sessionStringParameterName" value="iw_sessionstring"/>
 <property name="filterPatternVariableName" value="FilterPatterns"/>
 <!-- if you want a custom delimiter for the filter patterns, default is comma -->
 <property name="filterPatternDelimiterVariableName" value="FilterPatternDelimiter"/>
 </bean>

The reason we choose this one is because it does not require to forward any requests anywhere. we will duplicate the original task variables for Suffix, Processor, DestinationDirectory, FilterPatterns, FilterPatternDelimiter.

However, when we have the original CGI script task WebTask.do activated, struts will instantiate the bean for us, with the right values for the properties. Our external task currently does no such thing. Waht we shal do is create a custom CSURLExternalTask that extends the bean class specified and, upon instantiation sets the property values.

package com.acme.workflow;

public class PageConversionTask extends com.interwoven.livesite.workflow.task.PageConversionTask {

    private String mSuffixVariableName="Suffix";
    private String mProcessorVariableName="Processor";
    private String mDestinationDirectoryVariableName="DestinationDirectory";
    private String mFilterPatternVariableName="FilterPatterns";
    private String mFilterPatternDelimiterVariableName="FilterPatternDelimiter";
    private String mSessionStringParameterName="iw_sessionstring";
    private String mServicePath="/iw-cc/livesite/PageConversionServlet";

    public PageConversionTask() {
        super();
        super.setProcessorVariableName(mProcessorVariableName);
        super.setDestinationDirectoryVariableName(mDestinationDirectoryVariableName);
        super.setFilterPatternVariableName(mFilterPatternVariableName);
        super.setFilterPatternDelimiterVariableName(mFilterPatternDelimiterVariableName);
        super.setSessionStringParameterName(mSessionStringParameterName);
        super.setServicePath(mServicePath);
        super.setSuffixVariableName(mSuffixVariableName);
        System.out.println("instantiated com.acme.workflow.PageConversionTask");
    }

}

Compile the class into the customer_src toolkit and set the task variable ClassName to com.acme.workflow.PageConversionTask. The class will be executed by the workflow but as it stands will fail to transition properly. This is because the transition link names must be whatever the name of the task is plus “Success” or “Failure”. In our case it should be “Generate HTML Success” and “Generate HTML Failure”.

Another thing, this time concerning task paramerers. The way the task has been implemented, it is shomehow expecting task parameters, otherwise it reports an excpetion and fails. We need to pass some parameters to the task and it doesn’t even matter what they are. so for the URI, we can specify http://localhost/iw-cc/urlexternaltask?someParam=someValue. The task itself doesn’t even seem to use those parameters at all(!).

This little thing could be used to our advantage. If you look in our custom class, the names of the variables are hard-coded. This should be modified in the constructor to look ath the values of equivalent parameters, which could be passed with http://localhost/iw-cc/urlexternaltask?suffixVariableName=Suffix&destinationDirectoryVariableName=DestinationDirectory etc… To read the values would be done with a task.getParameter(“suffixVariableName”) statement.

Conclusion

You now can invoke page generation in workflows automatically without the intervention of users, in complete silence of the background process in the depth of your workflows.  Genius.

Often, your workflows will need to send emails to notify people that “stuff” is happening. Usually, this is done by email messaging. In this article, we’ll have a look at the method used to add the custom notification task to the workflow, generate the content of the email as XML and transform it using XSL into an HTML message before sending it. We shall keep it simple in this example and use a pre-determined email address (fictitious email address of laurent.picquet@acme.com).

Note: this is similar to a standard out-of-the-box notification task, except this method does not require another “target” task whose owner receives the email. It does however follow the same principles as those tasks. If you want to know how they are strung together, this article will further your understanding of them.

Add the task to the workflow

The task we need to create is a URLExternal task. Since our task is for custom notification, we will add the following variables:

ClassName: com.acme.workflow.CustomNotificationTask
stylesheetFilePath: /iwadmin/main/config/WORKAREA/shared/workflow/email/acme/customNotification.xsl
mailTo: laurent.picquet@acme.com

The URI property will be set to “http://localhost/iw-cc/urlexternaltask&#8221;. Update the name and the description of the task to be “custom notification”. Create the workflow links. Save and publish the workflow.

Generate the XML

We now have to create the class that will perform the custom notification task.

in iw-home/local/config/lib/content_center/customer_src/src/com/acme/workflow, create the file ustomNotificationTask.java. Edit this file and place this content into it:

package com.acme.workflow;

import com.interwoven.cssdk.common.CSClient;
import com.interwoven.cssdk.common.CSException;
import com.interwoven.cssdk.filesys.CSAreaRelativePath;
import com.interwoven.cssdk.filesys.CSFile;
import com.interwoven.cssdk.workflow.CSExternalTask;
import com.interwoven.cssdk.workflow.CSURLExternalTask;
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 *
 * @author Laurent Picquet
 */
public class CustomNotificationTask implements CSURLExternalTask {

 public void execute(CSClient client, CSExternalTask task, Hashtable params) throws CSException {
 // create the XML that will be transformed. It will display the list of attached files.
 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
 DocumentBuilder documentBuilder = null;
 Document document = null;
 try {
 documentBuilder = documentBuilderFactory.newDocumentBuilder();
 document = documentBuilder.newDocument();
 if (document != null) {
 Element mailContentElement = document.createElement("MailContent");
 document.appendChild(mailContentElement);
 mailContentElement.setAttribute("description", "Custom Notification");
 CSAreaRelativePath[] filesRelativePaths = task.getFiles();
 for (CSAreaRelativePath fileAreaRelativePath : filesRelativePaths) {
 CSFile attachedFile = task.getArea().getFile(fileAreaRelativePath);
 if (attachedFile != null) {
 Element fileElement = document.createElement("File");
 fileElement.setTextContent(attachedFile.getVPath().getPathNoServer().toString());
 mailContentElement.appendChild(fileElement);
 }
 }
 }
 } catch (ParserConfigurationException ex) {
 Logger.getLogger(CustomNotificationTask.class.getName()).log(Level.SEVERE, null, ex);
 } finally {
 task.chooseTransition("", "task completed.");
 }
 }
}

By way of explanation, this class so far creates a new XML document with a root element of MailContent and a File element for each one of the attached files. This might look like this:

<?xml version="1.0" ?>
<MailContent description="Custom Notification">
    <File>/default/main/acme/WORKAREA/work/sites/acme/index.page</File>
    <File>/default/main/acme/WORKAREA/work/sites/acme/about.page</File>
</MailContent>

Transform the XML using XSL and Send the message

This task now needs to send this data in an email. However, we don’t want to send an email containing XML, we want an email that contains HTML. The best way to transform XML to HTML is to use XSL.

We shall be using the an XSL file containing the following XSL transformations:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
 <xsl:output method="html"/>
 <xsl:template match="/MailContent">
 <html>
 <head>
 <title>Custom Notification</title>
 </head>
 <body>
 <p>Some sort of text goes here, if you want</p>
 <ul><xsl:apply-templates select="File" /></ul>
 </body>
 </html>
 </xsl:template>

<xsl:template match="File">
 <li><xsl:value-of select="." /></li>
</xsl:template>
</xsl:stylesheet>

Save the file in the WORKAREA, or somewhere accessible to TeamSite in a file called customNotification.xsl. Now that we have  the XML and the XSL, our java class must perform the transformation for us and add the result as HTML text to the content of the email. Fortunately for us, TeamSite comes with some utility classes that help us do that. Add the following code to your external class:

try {
 MailerConfig mailConfig = new MailerConfig();
 mailConfig.setHost(IWConfig.getConfig().getString("iwsend_mail", "mailserver"));
 mailConfig.setSubject("Custom notification");
 mailConfig.setSender(client.getCurrentUser().getEmailAddress()); /* in a task, we might want to use task.getOwner().getEmailAddress(); */
 String mailTo = task.getVariable("mailTo");
 mailConfig.addToRecipient(mailTo);

 if (document != null) {
 // acquire a transformation engine based on the specified xsl
 Transformer transformer = null;
 try {
 TransformerFactory transformerFactory = TransformerFactory.newInstance();
 String stylesheetFilePath = task.getVariable("stylesheetFilePath");
 if (stylesheetFilePath != null) {
     transformer = transformerFactory.newTransformer(new SAXSource(new InputSource(stylesheetFilePath)));
 }
 } catch (Exception e) {
 System.out.println("could not create a XSL transformer");
 e.printStackTrace();
 }

 StringWriter stringWriter = new java.io.StringWriter();
 StreamResult result = new StreamResult(stringWriter);
 try {
 transformer.transform(new DOMSource(document), result);
 mailConfig.addDataSource(new ByteArrayDataSource(stringWriter.toString().getBytes(), "text/html"));
 Mailer mailer = new Mailer(mailConfig);
 mailer.send();
 } catch (TransformerException ex) {
 Logger.getLogger(CustomNotificationTask.class.getName()).log(Level.SEVERE, null, ex);
 }
 }
 } catch (MessagingException ex) {
 Logger.getLogger(CustomNotificationTask.class.getName()).log(Level.SEVERE, null, ex);
 } catch (MailConfigException ex) {
 Logger.getLogger(CustomNotificationTask.class.getName()).log(Level.SEVERE, null, ex);
 }

Note that we are doing the transformation ourselves. There is also another utility that can do the trasnformation for us.

When you have a date field or any other date represented as strings and want to re-format them in a different format, it is still feasible to do so using XSL.

As an example, take the date “2009-08-14T00:00:00+09:00”. This is not very user friendly but “14 August 2009 “would be a lot more readable. Let’s start with a sample XML document to transform into HTML using XSL that we’ll call articles.xml.

<?xml version="1.0" ?>
<articles>
    <article>
        <title>Lorem ipsum dolor sit amet</articles>
        <description>Morbi elit quam, porttitor non aliquet ultrices, elementum quis erat. Nulla tempus lorem eget tortor accumsan cursus. Fusce quis massa laoreet mi suscipit rutrum. Duis id tincidunt eros. Aenean feugiat consectetur ligula vitae facilisis. Morbi dapibus velit eu mauris rutrum ut ornare lorem tempor. Aenean ut sapien a turpis imperdiet laoreet. Fusce iaculis, eros at ultricies vehicula, dui velit condimentum eros, a suscipit urna justo sed augue. Phasellus venenatis varius feugiat. Aliquam nec orci leo.</description>
        <date>2009-08-14T00:00:00+09:00</date>
    </article>
</articles>

Let’s create the first version of the XSL and we’ll save it in articles.xsl.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
 <xsl:output method="html"/>

 <xsl:template match="/">
 <html>
 <head>
 <title>newstylesheet.xsl</title>
 </head>
 <body>
 </body>
 </html>
 </xsl:template>

</xsl:stylesheet>

We can extract portions of the date uisng the substr function for day, month and year and we can use the call-templates functionality to display the proper month names (e.g. January) by passing the month number (e.g. 01) as a parameter to the template.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
 <xsl:output method="html"/>

 <xsl:template match="/articles">
 <html>
 <head>
 <title>newstylesheet.xsl</title>
 </head>
 <body>
 <xsl:apply-templates select="article" />
 </body>
 </html>
 </xsl:template>

<xsl:template match="article">
 <xsl:value-of select="title" /> (<xsl:apply-templates select="date" />)
</xsl:template>

<xsl:template match="date">

 <!--
 year: <xsl:value-of select="substring(.,1,4)" /><br />
 month:<xsl:value-of select="substring(.,6,2)" /><br />
 day: <xsl:value-of select="substring(.,9,2)" /><br />
 -->

 <xsl:value-of select="substring(.,9,2)" />
 <!-- replacing a month number by its month name -->
 <xsl:call-template name="number-to-month">
 <xsl:with-param name="month"><xsl:value-of select="substring(.,6,2)" /></xsl:with-param>
 </xsl:template>
 <xsl:value-of select="substring(.,1,4)" />
</xsl:template>

<!-- replaces a month number by its month name -->
<xsl:template name="number-to-month">
 <xsl:param name="month"/>
 <xsl:choose>
 <xsl:when test="$month = '01'">January</xsl:when>
 <xsl:when test="$month = '02'">February</xsl:when>
 <xsl:when test="$month = '03'">March</xsl:when>
 <xsl:when test="$month = '04'">April</xsl:when>
 <xsl:when test="$month = '05'">May</xsl:when>
 <xsl:when test="$month = '06'">June</xsl:when>
 <xsl:when test="$month = '07'">July</xsl:when>
 <xsl:when test="$month = '08'">August</xsl:when>
 <xsl:when test="$month = '09'">September</xsl:when>
 <xsl:when test="$month = '10'">October</xsl:when>
 <xsl:when test="$month = '11'">November</xsl:when>
 <xsl:when test="$month = '12'">December</xsl:when>
 </xsl:choose>
 </xsl:template>

</xsl:stylesheet>

CSS styles can be specified as inline style for the document, in the head using … tags. They can also be attached to the document by using the tag. Lastly, styles can be applied to tags directly using the style attribute for the HTML element.

When developing components, it is often very tempting to place styling inline, using the appearance XSL input area as style attributes to elements. While this is fine for the development phase, styles should be extracted to a stylesheet document baring the same name as the component.

From there, there are two solutions. Either the users add the stylesheet individually to each page, or the stylesheet is added to the default.site directly and by extension to all the pages. Although very legal, in practice, there is something that people don’t like to see 30 CSS files attached to a single HTML document. To paliate to this “perception” problem, a nice solution is to create 1 css document that uses the @import functionality of the CSS, thus giving the appearance that only 1 CSS document is called by the page.

Sometimes, we would like one workflow to appear before another but since they’re stored alphabetically, we’re a bit stuck in what we can do. Some people resort to prepending numbers or letters to the display name of the workflow so order them.

We can order them nicely on the page but prepending a space to the display name of the workflow. Since the form displays the workflow name using HTML, it strips redundant spaces, thus making them ordered. Space comes before normal lettering in their orders, so the more spaces you add to the display name, the more towards the top it will appear.