Requirements

Prerequisite knowledge

LiveCycle services, process creation in Workbench, form design in Designer, thorough understanding of the Managed Review and Approval Solution Accelerator.

 

Additional Requirements:

Solution Accelerators 9.5

User level

Intermediate

The purpose for the RSS custom component was to handle a RSS XML file that was specific to the Managed Review & Approval application by containing a list of all comments made on a review document. The XML file was stored in Contentspace and the goal of the component was to retrieve the file and determine how many comments it contained per reviewer. This value would then be used for the Review Tracking Sheet to populate the number of comments field beside the respective reviewer. This would be effective tool because it would allow the review initiator to get a quick overview of how many changes they would have to consider, who suggested those changes, and who are actively participating in reviews.

A custom component was necessary because there was no available functionality in the solution accelerator or LiveCycle that would parse an XML and return the needed outputs.

The component used the Quickstart and custom component methodology and was developed in Java.

This article will outline how to create the custom component and how to integrate this feature into the existing Review Tracking Sheet process.

Structure

The component includes the ParseService interface which defined the parse method, the ParseServiceImpl which provided the functionality, a custom Comment object was created for testing purposes and the LivecycleImpl and BootStrapImpl are explained in the QuickStart reference attached at the bottom.

Technology

Some of the technology that this article will discuss and use:

  • Java
  • LiveCycle Workbench ES2
  • Livecycle ES2 server

Enhancements and pitfalls

When this component was deployed into Workbench it was required that the server name be hardcoded into the URL because there was no other way to draw the server name from Designer. In addition the component also logs in as a hardcoded person. If this component is to ever move from the myservername-w2k3 server and my username (alephill) becomes deactivated these features will need to be updated. If they are not the component will stall and cause reviews not to be updated after a stage it completes.

Modifying the Review Tracking Sheet In Designer

The Review Tracking Sheet is a form that was created in Designer as a XDP file (see Figure 1). We must first re-configure the bindings to recognize the changes that are about to occur.

To edit the Review Tracking Sheet follow these steps:

  1. Navigate to Window-> Show View ->Application.
  2. Select ManagedReviewAndApproval-Sample and expand the application version you are working on currently.
  3. Expand resources->forms and double click on ReviewTrackingSheet to open in LiveCycle Designer.
  4. A column heading must be added to the Reviewers subform with the heading "No Comments". It is best practice to copy and paste another heading so that this heading is identical format to the others.
  5. A text field must then be made for the respective information. Again, it is best practice to copy and paste since the look of the other text fields will be copied.

Change the binding from the original copy and pasted binding to $.no_of_comments and change the name to something descriptive (see Figure 2).

  1. Save and check in the ReviewTrackingSheet.

Creating the Custom Component

To parse the RSS XML the way we would like, it is most efficient to write a quick custom component in Java and install its service. Please refer to this documentation to find more on how custom components are configured and should be installed.

First we must define the interface for our service – I named mine CommentService.

package com.adobe.livecycle.parserss; import java.io.IOException; import java.util.ArrayList; import com.adobe.livecycle.contentservices.client.impl.CRCResultImpl; public interface CommentService { /** * @param args */ public int parse(String URL, String username, String password) throws IOException; public ArrayList<String> getFileNames(CRCResultImpl files); }

The CommentServiceImpl implementation class is also required.

package com.adobe.livecycle.parserss; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import javax.xml.parsers.*; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.adobe.livecycle.contentservices.client.impl.CRCResultImpl; public class CommentServiceImpl implements CommentService { /* * parse() method will take a rss file belonging to a person */ public int parse(String URL, String username, String password) throws IOException { Authenticator.setDefault(new MyAuthenticator(username, password) ); URL tempfile = new URL(URL); URLConnection con = tempfile.openConnection(); InputStream in = con.getInputStream(); ArrayList<Comment> reviewComments = new ArrayList<Comment>(); try{ DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); Document docu = docBuilder.parse(in); docu.getDocumentElement().normalize(); NodeList commentList = docu.getElementsByTagName("item"); NodeList authorList = docu.getElementsByTagName("author"); int numComments = commentList.getLength(); in.close(); System.out.println(numComments); for(int i = 0; i <numComments; i++){ Node tempItem = commentList.item(i); Node tempAuthor = authorList.item(i); System.out.println(tempItem.getFirstChild().getTextContent()); System.out.println(tempAuthor.getFirstChild().getTextContent()); Comment comment = new Comment("user", date(), tempItem.getFirstChild().getTextContent().toString()); reviewComments.add(comment); System.out.println(((Comment)reviewComments.get(0)).toString()); System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); } } catch(Exception e){ e.printStackTrace(); } System.out.println(reviewComments.size()); return reviewComments.size(); } static class MyAuthenticator extends Authenticator { private String username; private String password; public MyAuthenticator(String us, String pass){ this.username = us; this.password = pass; } public PasswordAuthentication getPasswordAuthentication() { return (new PasswordAuthentication(username,password.toCharArray())); } } public static String date() { Calendar cal = Calendar.getInstance(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(cal.getTime()); } @Override public ArrayList<String> getFileNames(CRCResultImpl files) { ArrayList<String> result = new ArrayList<String>(); System.out.println(files.getTitle()); return null; } }

A custom data structure was also used in the CommentServiceImpl class and needs to be defined.

package com.adobe.livecycle.parserss; public class Comment { private String userName; private String dateString; private String comment; public Comment(){ this.userName = null; this.dateString = null; this.comment = null; } public Comment(String username, String date, String comment){ this.userName = username; this.dateString = date; this.comment = comment; } public String getUserName(){ return userName; } public String getDateString(){ return dateString; } public String getComment(){ return comment; } public void setUserName(String username){ this.userName = username; } public void setDateString(String date){ this.dateString = date; } public void setComment(String comment){ this.comment = comment; } public String toString(){ String result = "Username: " + userName + " Date: " + dateString + " Comment: " + comment; return result; } }

In addition is it necessary to include the BootStrapImpl and the LivecycleImple classes in the project.

LivecycleImpl

package com.adobe.livecycle.parserss; import java.util.logging.Level; import com.adobe.idp.dsc.component.ComponentContext; import com.adobe.idp.dsc.component.LifeCycle; import com.adobe.logging.AdobeLogger; public class LifeCycleImpl implements LifeCycle { private static final AdobeLogger logger = AdobeLogger.getAdobeLogger(LifeCycleImpl.class); private ComponentContext m_ctx; //Sets the component's context public void setComponentContext(ComponentContext aContext) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "setComponentContext: " + aContext.getComponent().getComponentId()); } m_ctx = aContext; } //Invoked when the component is started public void onStart() { logger.log(Level.INFO, "Called onStart: " + m_ctx.getComponent().getComponentId()); } //Invoked when the component is stopped public void onStop() { logger.log(Level.INFO, "Called onStop: " + m_ctx.getComponent().getComponentId()); } }

BootstrapImpl

package com.adobe.livecycle.parserss; import java.util.logging.Level; import com.adobe.idp.dsc.component.Bootstrap; import com.adobe.idp.dsc.component.BootstrapContext; import com.adobe.logging.AdobeLogger; public class BootstrapImpl implements Bootstrap{ private static final AdobeLogger logger = AdobeLogger.getAdobeLogger(BootstrapImpl.class); private BootstrapContext m_ctx; public void setBootstrapContext(BootstrapContext aCtx) { logger.log(Level.INFO, "Set bootstrap context: " + aCtx.getComponent().getComponentId()); m_ctx = aCtx; } //Invoked when the component is uninstalled public void onUnInstall() { logger.log(Level.INFO, "Called onUnInstall: " + m_ctx.getComponent().getComponentId()); } //Invoked when the component is installed public void onInstall() { logger.log(Level.INFO, "Called onInstall: " + m_ctx.getComponent().getComponentId()); } }

Finally you must configure the components.xml.

<component xmlns="http://adobe.com/idp/dsc/component/document"> <!-- Unique id identifying this component --> <component-id>com.adobe.livecycle.parserss</component-id> <!-- Version --> <version>1.0</version> <!-- bootstrap implementation class --> <bootstrap-class>com.adobe.livecycle.parserss.BootstrapImpl</bootstrap-class> <!-- lifecycle implementation class --> <lifecycle-class>com.adobe.livecycle.parserss.LifeCycleImpl</lifecycle-class> <!-- Start of the Service definition --> <services> <!-- Unique name for service descriptor. The value is used as the default name for deployed services --> <service name="AlexCommentService"> <!-- service implementation class definition --> <implementation-class>com.adobe.livecycle.parserss.CommentServiceImpl</implementation-class> <!-- description --> <description>Allows you to parse RSS xml. Used to determine the number of comments in MRA implementation.</description> <!-- automatically deploys the service and starts it after installation --> <auto-deploy service-id="AlexCommentService" /> <operations> <!-- method name in the interface--> <operation name="parse"> <!-- input parameters to the "send" method --> <input-parameter name="url" title="RSS URL" type="java.lang.String"> <hint>A URL to the RSS file.</hint> </input-parameter> <input-parameter name="username" title="LC Server User Name" type="java.lang.String"> <hint>Username to access the server</hint> </input-parameter> <input-parameter name="password" title="LC Server Password" type="java.lang.String"> <hint>Password to access the server</hint> </input-parameter> <output-parameter name="commentCount" title="Number of Comments" type="java.lang.Integer"> <hint>The count of comments in rss file</hint> </output-parameter> <faults> <fault name="I/O Exception" type="java.io.IOException" /> <fault name="Exception" type="java.lang.Exception" /> </faults> <hint>Counts the number of comments in an MRA comments RSS file</hint> </operation> </operations> </service> </services> </component>

The structure should looks similar to the following illustration (see Figure 3).

To deploy the componet, follow the steps below:

  1. Once your strcuture looks similar to Figure 2, export this project to a JAR file by Select-> Export.
  2. Choose Jar file and select Next.
  3. Make sure all files are included in the Jar and select Finish.
  4. To install your new component, open Workbench and login to your LiveCycle server using proper credentials (see Figure 4).
  1. Select Window -> Show View -> Components.
  2. Right-Click the root Components folder and select Install Component.
  3. In the navigator window find your new jar with your custom component and click Open.
  4. Expand the Components folder, find your new Service and right-click Start Component.

Creating a Process to Check the Initiator

When accessing the XML files in Contentspace, it must also be taken into consideration that the review initiator will always have an xml file created. To ensure the Review Tracking Sheet is populated correctly a process must be made to evaluate if the review initiator is also a reviewer and whether or not we should consider there file. I named mine Initiator Is Review Check (see Figure 5).

Here is the list of variables to add:

  • reviewContextXML: xml, Input—the review context xml
  • currentStage: int, Input—the numerical representation of the current stage
  • totalreviewers:int—the number of reviewers in the current stage
  • initiatorname: string—the name of the initiator
  • currentReviewerName: string—the name of the current reviewer in the iteration
  • counter: int—loop counter

Follow the steps below to create the Initiator is Reviewer Check process:

  1. Use a default startpont
  2. Place a Set Value process named Set Loop immediately after the startpoint with the following mappings:
    1. Mapping: /process_data/@counter -> 1
    2. Mapping: /process_data/@totalreviewers -> count(/process_data/reviewContextXML/review_context/stages/stage[/process_data/@currentStage +0 ]/reviewers/reviewer)
    3. Mapping: /process_data/@isReviewer -> "false"
    4. Mapping: /process_data/@initiatorName -> /process_data/reviewContextXML/review_context/initiator/@canonical_name
  3. Place a Decision Point that will go through all reviewers until the counter reaches the total number of reviewers in the stage. Evaluate the condition route first.
    1. Condition to enter into the loop: /process_data/@counter <= /process_data/@totalreviewers
    2. The opposing route will connect to NoOp
  4. After the conditional route place a Set Value process called Get Current Review Name with the mapping:
    1. Mapping: /process_data/@currentReviewerName -> /process_data/reviewContextXML/review_context/stages/stage[/process_data/@currentStage +0 ]/reviewers/reviewer[/process_data/@counter]/completed_by/@canonical_name
  5. Place a second Decision Point named Is Reviewer that evaluates the conditional route first.
    1. Condition: /process_data/reviewContextXML/review_context/initiator/@canonical_name = /process_data/reviewContextXML/review_context/stages/stage[/process_data/@currentStage +0 ]/reviewers/reviewer[/process_data/@counter]/completed_by/@canonical_name
    2. The second route will jump to Increment Counter
  6. After the Is Review condition place a Set Value process named Set Result which has the following mapping:
    1. Mapping: /process_data/@isReviewer -> "true"
  7. Then place Set Value process to increment the counter:
    1. Mapping: /process_data/@counter -> /process_data/@counter + 1
  8. Place a NoOp Decision Point below the Loop Check Decision Point that will finish the process.

Integrating with the Assign Tasks Processes

Finally we have to integrate our new processes into the Review Tracking Sheet Creation Process (see Figure 6). LiveCycle Contentspace ES2 automatically generates an XML file for each reviewer that leaves a comment in the review document so the process will loop through the files and identify the number of comments in each. If someone did not leave any comments they will have a default 0 value.

It is now necessary to add more variables to the process. To do so:

  1. Select Window -> Show View -> Variables
  2. In the variables panel select the green plus button.
  3. Fill out the dialog box with the following details and select OK

Note: The green box will be disabled if you are in recording mode.

Here is the list variables to add to the process to accommodate for the new fucntionality:

  • Username: String —username to be authenticated into the Livecycle server
  • Password: String—password to be authenticated into the LiveCycle server
  • rssURL: String—the URL to the Comment XML file on Contentspace
  • reviewerName: String—the e-mail name of the current reviewer selected
  • reviewCounter: int—a counter that stores the number of the reviewers for the loop
  • reviewCount:int—the number of total reviewers who left comments
  • reviewID: String—the review ID of the current review
  • reviewContextXML: xml—the review context in XML form with schema
  • numComments: int—the number of comments in a review
  • currentStage:int—an integer representing which stage of the review current in progress
  • currentReviewer: CRCResultImpl—a contentspace CRCResultImpl object representing the current reviewer
  • commenterList: list <CRCResultImpl>—a list that stores information about the reviewers
  • initiatorIsReviewer: Boolean—a flag to indicate whether the initiator is a reviewer

The following steps detail how to customize the process:

  1. Place the Set Value after the default startpoint. All connections are non conditionally with the exception of the Check Reviewer Count Decision Point to the Set New Reviewer process.
  2. Get Create XML is a Set Value process.
    1. Mapping: /process_data/reviewContextXML -> /process_data/@inReviewContext
    2. Mapping: /process_data/@currentStage -> /process_data/reviewContextXML/review_context/current_stage
  3. getSpaceContents is a GetSpaceContents process
    1. Input: Store Name = SpacesStore
    2. Input: Space ID/Path = (XPath) concat("/Company Home/RCA/", /process_data/reviewContextXML/review_context/review_id,"/v", /process_data/reviewContextXML/review_context/revision, "/ReviewDocument_",/process_data/@currentStage, ".pdf__" ,/process_data/reviewContextXML/review_context/review_id, "/")
    3. Input: Check the "Retrieve Files Only Checkbox".
    4. Output: Content List = commenterList
  4. Set Reviewer Count is a Set Value process
    1. Mapping: /process_data/@reviewerCounter -> 1
    2. Mapping: /process_data/@reviewerCount -> get-collection-size(/process_data/commenterList)
    3. Mapping: /process_data/currentReviewer -> /process_data/commenterList[/process_data/@reviewerCounter + 0]
    4. Mapping: /process_data/@NoStages -> count(/process_data/reviewXML/review_context/stages/stage)
    5. Mapping: /process_data/@initiatorName -> concat(/process_data/reviewContextXML/review_context/initiator/@canonical_name,".at.adobe.com.xml")
    6. Mapping: /process_data/@reviewerPosition -> 1
  5. Place a Initiator Is Reviewer Check process we made in the last section called Check if the Initiator is a Reviewer
    1. Input: currentStage = currentStage variable
    2. Input: reviewContextXML = reviewContextXML variable
    3. Output: isReviewer = initiatorIsReviewer variable
  6. Check Reviewer Decision Point, evaulate <ReviewCount then >ReviewCount
    1. <ReviewCount: /process_data/@reviewerCounter < = /process_data/@reviewerCount
    2. >ReviewCount: No Condition
  7. Set New Reviewer is a Set Value process
    1. Mapping: /process_data/@reviewerName -> /process_data/currentReviewer/object/@nodeName
    2. Mapping: /process_data/@reviewID -> /process_data/reviewContextXML/review_context/review_id
  8. Place a Decision Point called Initiator Review Check, evaluate the condition route first
    1. Condition route: /process_data/@initiatorIsReviewer = "false" AND /process_data/@initiatorName = /process_data/@reviewerName will link to the Set Value process Increment Reviewer Counter
    2. The Non-Conditional route will enter into the loop
  9. Following the NON-Conditional route, Set Username and Password is a Set Value process
    1. Mapping: /process_data/@username -> YOUR USERNAME
    2. Mapping: /process_data/@password -> YOUR PASSWORD
    3. Mapping: /process_data/@rssURL -> concat("http://yourservername:yourport/contentspace/webdav/RCA/", /process_data/reviewContextXML/review_context/review_id, "/v", /process_data/reviewContextXML/review_context/revision, "/ReviewDocument_", /process_data/@currentStage, ".pdf__", /process_data/reviewContextXML/review_context/review_id, "/", /process_data/@reviewerName)
  10. Parse is a Parse custom process.
    1. Input: RSS URL = rssURL
    2. Input: LC Server Username = username
    3. Input: LC Server Password = password
    4. Output: Number of Comments = numComments
  11. Set Number of Comments is a Set Value process.
    1. Mapping: /process_data/reviewContextXML/review_context/stages/stage[/process_data/@currentStage +0 ]/reviewers/reviewer[/process_data/@reviewerPosition + 0 ]/no_of_comments -> /process_data/@numComments
  12. Increment Review Position is a Set Value process.
    1. Mapping: /process_data/@reviewerPosition -> /process_data/@reviewerPosition +1
  13. Increment Reviewer Counter is a Set Value process.
    1. Mapping: /process_data/@reviewerCounter -> /process_data/@reviewerCounter + 1
    2. Mapping: /process_data/currentReviewer -> /process_data/commenterList[/process_data/@reviewerCounter+0]
  14. Create RTS is a generatePDFOutput process and is already given. There is no change required.

Where to go from here

To learn more about how to develop comments, refer to Programming in LiveCycle ES2.5.