07-15-2006 03:27 PM
main(){
if (document != null) {
// execution by a rule, it should be on the INBOUND event in the folder where incoming documents arrive
// workflow instance initialization
// - we create a new workflow instance (using a custom type)
…
// - we initialize the properties of this object
…
// send HTML stored message to the first actor(s)
…
return;
} else {
// this script has been called by an HTTP get from a HTML stored message
// get info from args (caller, current step in the workflow, action name, action arguments, ?
…
// retrieve the workflow object and get info on current workflow instance
…
// treat actions
switch(workFlowStep){
case 1:
switch(actionName){
case "accept": // for example
// do some business process
…
// create and send stored messages to next actors
…
break;
…
}
break;
case 2:
…
break;
…
Case <n>:
…
break;
}
If (success){
// if all is OK, we delete the calling stored HTML message
…
// we send a direct answer to the user
return("<HTML>Your request has been treated successfully</HTML>"); // for example
} else {
return(errorMessage);
}
}
main();
07-20-2006 03:50 AM
07-23-2006 12:34 PM
<model name="vote:contentmodel" xmlns="http://www.alfresco.org/model/dictionary/1.0">
<description>Vote Workflow Content Domain Model</description>
<author>A.Weber</author>
<published>2006-07-18 20:00</published>
<version>1.0</version>
<imports>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
</imports>
<namespaces>
<namespace uri="vote_wf.model.content.1.0" prefix="vote"/>
</namespaces>
<!– ===== –>
<!– Types –>
<!– ===== –>
<types>
<!– ========= –>
<!– Documents –>
<!– ========= –>
<!– object used to store information about a vote workflow –>
<type name="vote:vote_wf">
<title>vote WF instance data</title>
<parent>cm:content</parent>
<properties>
<property name="vote:status">
<title>status of the workflow to treat a vote</title>
<type>d:text</type>
</property>
<property name="vote:voters">
<title>List of users called to vote</title>
<type>d:text</type>
</property>
<property name="vote:refusers">
<title>List of users having refused the submitted doc vote</title>
<type>d:text</type>
</property>
<property name="vote:accepters">
<title>List of users called having accepted the submitted doc</title>
<type>d:text</type>
</property>
<property name="vote:votersWithoutOpinion">
<title>List of users having no option</title>
<type>d:text</type>
</property>
</properties>
</type>
<!– type for a votable document –>
<type name="vote:votableDocument">
<title>VotableDocument</title>
<parent>cm:content</parent>
<mandatory-aspects>
<aspect>vote:votable</aspect>
</mandatory-aspects>
</type>
</types>
<aspects>
<!– Definition of new Content Aspect: votable –>
<aspect name="vote:votable">
<title>votable</title>
<associations>
<association name="vote:votersAssoc">
<title>voters</title>
<source>
<role>vote:voteObject</role>
<mandatory>false</mandatory>
<many>true</many>
</source>
<target>
<class>cm:person</class>
<role>vote:voter</role>
<mandatory>false</mandatory>
<many>true</many>
</target>
</association>
</associations>
</aspect>
</aspects>
</model>
<alfresco-config>
<!– ====================================== –>
<!– Custom Content Types in the Web Client –>
<!– ====================================== –>
<config evaluator="string-compare" condition="Content Wizards">
<content-types>
<!– vote WF –>
<type name="vote:votableDocument"/>
</content-types>
</config>
<!– ====================================== –>
<!– Node Type Properties in the Web Client –>
<!– ====================================== –>
<!– . Vote Workflow data –>
<config evaluator="node-type" condition="vote:vote_wf">
<property-sheet>
<show-property name="vote:status"
show-in-edit-mode="false"/>
<show-property name="vote:voters"
show-in-edit-mode="false"/>
<show-property name="vote:accepters"
show-in-edit-mode="false"/>
<show-property name="vote:refusers"
show-in-edit-mode="false"/>
<show-property name="vote:votersWithoutOpinion"
show-in-edit-mode="false"/>
</property-sheet>
</config>
<!– =============================== –>
<!– Custom "Add Aspect" Rule Action –>
<!– =============================== –>
<config evaluator="string-compare" condition="Action Wizards">
<!– List of types shown in the is-subtype condition –>
<subtypes>
<type name="vote:votable"/>
</subtypes>
<!– List of content types shown in the specialize-type action –>
<specialise-types>
<type name="vote:votableDocument"/>
</specialise-types>
<!– List of aspects to show in the add/remove features action –>
<!– and the has aspect condition –>
<aspects>
<aspect name="vote:votable"/>
</aspects>
</config>
<!– =================================== –>
<!– Aspect Properties in the Web Client –>
<!– =================================== –>
<!– Votable –>
<config evaluator="aspect-name" condition="vote:votable">
<property-sheet>
<show-association name="vote:votersAssoc"
show-in-edit-mode="true"
show-in-view-mode="true"/>
</property-sheet>
</config>
</alfresco-config>
<html>
<head>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
</head>
<body>
<form action="<js>getUrlForScriptCall();</js>">
<input type="hidden" name="msgPath" value="<js>msgPath</js>">
<input type="hidden" name="votedDocName" value="<js>document.name</js>">
<input type="hidden" name="votedDocPath" value="<js>document.displayPath</js>">
The user <js>person.properties["cm:userName"]</js> submit the document <a href="/alfresco<js>document.url</js>"><js>document.name</js></a> to vote<br></br>
<input type="radio" name="voteDecision" value="accepted">Your vote is : Yes<br></br>
<input type="radio" name="voteDecision" value="rejected"> Your vote is : NO<br></br>
<input type="radio" name="voteDecision" value="noOpinion">You have no opinion
<input type="submit" name="submit" value="Submit vote">
</form>
</body>
</html>
<html>
<head>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
</head>
<body>
The document <js>document.name</js> obtained the following result in the vote process : <js>wfData.properties["vote:status"]</js><BR></BR>
This document has been submitted to : <js>wfData.properties["vote:voters"]</js><BR></BR>
This document has been accepted by : <js>wfData.properties["vote:accepters"]</js><BR></BR>
This document has been refused by : <js>wfData.properties["vote:refusers"]</js><BR></BR>
The following voters have no opinion : <js>wfData.properties["vote:votersWithoutOpinion"]</js><BR></BR>
</body>
</html>
//========================================================
// log management
//========================================================
var debug = false; // should be set to false when this script is stored in CVS
var logFile = null;
var logFolderPath = "test_vote_WF";
var logFilename = script.name + ".log";
// Log into a file with prefix: timestamp
function log(msg)
{
// find or create the log file
var logFileFolder;
if (logFile == null){
logFileFolder = companyhome.childByNamePath(logFolderPath);
if (logFileFolder == null) return;
logFile = logFileFolder.childByNamePath(logFilename);
}
if (logFile == null){
if (true || logFileFolder.hasPermission("Write")){
logFile = logFileFolder.createFile(logFilename);
}
}
// append with a timestamp
if (logFile != null && logFile.hasPermission("Write")){
logFile.content += new Date().toLocaleString() +
"; " + msg +
"\r\n";
}
}
//========================================================
// Manage lists
//========================================================
function getNbItemsInList(list){
if (list == null) return(0);
if (list == "") return(0);
if (list.indexOf(",") <= 0) return(1);
return(list.split(",").length);
}
/*
Merge two list of strings concatenated in a string with the char "," as separator
No doublons are allowed
*/
function mergeTwoLists(list1,list2){
if (list1 == null || list1=="") return(list2);
if (list2 == null || list2=="") return(list1);
var list2Items = list2.split(",");
var i;
var list2Item;
for (i in list2Items){
list2Item = list2Items[i];
// if the item is not in list1, we add it
if ( ("," + list1 + ",").indexOf("," + list2Item + ",") < 0 ){
list1 += "," + list2Item;
}
}
return(list1);
}
/*
check if a string item is in a list
*/
function isInList(list1,item){
if(list1 == null || list1=="") return(false);
if(item==null || item=="") return(false);
return( ("," + list1 + ",").indexOf("," + item + ",") >= 0);
}
/*
add an item to a list if this item is not in the list
*/
function addItemToList(list1,item){
if (list1 == null) list1 = "";
if (item == null || item == "") return(list1);
if (list1 == "") return(item);
list1 = list1 + "," + item;
return(list1);
}
/*
remove an item from a list (if this item is in the list)
*/
function removeItemFromList(list1,item){
var len;
if (list1 == null || list1=="") return(list1);
if (item == null || item=="") return(list1);
list1 = "," + list1 + ",";
list1 = list1.replace(","+item+",",",");
list1 = list1.substr(1);
len = list1.length;
list1 = list1.substr(0,len-1);
return(list1);
}
/*
add two exclusive lists (items in list1, can't be in list2)
*/
function addExclusiveLists(list1,list2){
var resultList = "";
if (list1 != "") {
resultList = list1;
if (list2 != ""){
resultList += "," + list2;
}
} else {
resultList = list2;
}
return(resultList);
}
/*
Check if all elements of list1 are in list2 and vice versa
*/
function areListsEqual(list1, list2){
var items;
var i;
var item;
if (list1 == "" && list2 == "") return(true);
if (list1 == "" && list2 != "") return(false);
if (list2 == "" && list1 != "") return(false);
if (list1.length != list2.length) return(false);
items = list1.split(",");
for (i in items){
item = items[i];
if (item != "" && !isInList(list2,item)) return(false);
}
return(true);
}
/*
Return elements present in list1 and in list2
*/
function intersectionList(list1, list2){
var items;
var i;
var item;
var resultList = "";
if (list1 == "") return("");
if (list2 == "") return("");
items = list1.split(",");
for (i in items){
item = items[i];
if (isInList(list2,item)){
if (resultList == "") resultList = item;
else resultList += "," + item;
}
}
return(resultList);
}
// ===================================================================
// HTML template management
// ===================================================================
function replaceHostedJavaScript(content){
var posBeg;
var posEnd;
var jsExpr;
var jsExprEvaluated;
var tryCatch= false;
while(true){
posBeg = content.indexOf("<js>");
if(posBeg>0){
posEnd = content.indexOf("</js>",posBeg);
if (posEnd>0){
jsExpr = content.substr(posBeg+4, posEnd-posBeg-4);
if (tryCatch){
try{
jsExprEvaluated = eval(jsExpr + ";");
}
catch(exception){
jsExprEvaluated = "exception in the evaluation of " + jsExpr + " :" + exception;
}
} else jsExprEvaluated = eval(jsExpr + ";");
content = content.substr(0,posBeg) + jsExprEvaluated + content.substr(posEnd+5);
}
}
else return(content);
}
return(content);
}
function getUrlForScriptCall(){
var url = "";
url += "/alfresco/command/script/execute/" + script.nodeRef;
var expr = /\:\/\//;
url = url.replace(expr,"/");
return(url);
}
// ==================================================================
function getFileContent(folder, fileName){
return("" + folder.childByNamePath(fileName).content);
}
/*
Send a message in a mailBox
*/
function sendMsg(msgName, htmlFileName,mailBoxFolderPath){
var htmlFolder;
var mailBoxFolder;
var content;
var msgFile;
htmlFolder = companyhome.childByNamePath(HTML_TEMPLATE_FOLDER_PATH);
if (htmlFolder== null) {
log("sendMsg() - ERROR - htmlFolder not found (path=\"" + HTML_TEMPLATE_FOLDER_PATH + "\")");
return;
}
mailBoxFolder = companyhome.childByNamePath(mailBoxFolderPath);
if (mailBoxFolder== null) {
log("sendMsg() - ERROR - mailBoxFolder not found");
return;
}
msgFile = mailBoxFolder.childByNamePath(msgName);
if (msgFile != null) msgFile.remove();
msgFile = mailBoxFolder.createFile(msgName);
if (msgFile== null) {
log("sendMsg() - ERROR -can't create msg file");
return;
}
msgPath = mailBoxFolderPath + "/" + msgName;// var used by the JS embedded in the HTML template
content = getFileContent(htmlFolder,htmlFileName);
content = replaceHostedJavaScript(content);
msgFile.content = content;
return("");
}
/*
Delete a message from a mailBox
*/
function removeMsg(msgName, mailBoxFolderPath){
var mailBoxFolder;
var msgFile;
htmlFolder = companyhome.childByNamePath(HTML_TEMPLATE_FOLDER_PATH);
if (htmlFolder== null) {
log("removeMsg() - ERROR - htmlFolder not found (path=\"" + HTML_TEMPLATE_FOLDER_PATH + "\")");
return;
}
mailBoxFolder = companyhome.childByNamePath(mailBoxFolderPath);
if (mailBoxFolder== null) {
log("removeMsg() - ERROR - mailBoxFolder not found");
return;
}
msgFile = mailBoxFolder.childByNamePath(msgName);
if (msgFile != null) msgFile.remove();
return("");
}
// ==================================================================
/*
Retrieve the names the voters referred in the "vote:votersAssoc" association
@param votableDocument document with the aspect "vote:votable"
@return this list or null if it fails
*/
function getVotersNames(votableDocument){
var voterName;
var voters;
var folderRef;
var folderName;
var votersNames = new Array();
var i;
var iFolder=0;
if (!votableDocument.hasAspect("vote:votable")) return(null);
voters = votableDocument.assocs["vote:votersAssoc"];
for (i in voters){
voterName = voters[i].properties["cm:userName"];
votersNames[iFolder++] = voterName;
}
return(votersNames);
}
/*
Retrieve the names the home folder of each voter referred in the "vote:votersAssoc" association
@param votableDocument document with the aspect "vote:votable"
@return this list or null if it fails
*/
function getVotersHomeFolders(votableDocument){
var voter;
var voters;
var folderRef;
var folderName;
var votersFolders = new Array();
var i;
var iFolder=0;
if (!votableDocument.hasAspect("vote:votable")) return(null);
voters = votableDocument.assocs["vote:votersAssoc"];
for (i in voters){
voter = voters[i];
folderRef = voter.properties["cm:homeFolder"];
if (folderRef != null && folderRef.hasPermission("Read")){
folderName = folderRef.parent.name + "/" + folderRef.name;
votersFolders[iFolder++] = folderName;
}
}
return(votersFolders);
}
sendWfMessagesToVoters
/*
Send a WF message to each voter
Attention : I suppose the homeFolders are grand children of the "company_home" folder (TO REVIEW).
*/
function sendWfMessagesToVoters(docToVote){
var votersFoldersNames;
var i;
var voterFolderName;
var targetFolder;
votersFoldersNames = getVotersHomeFolders(docToVote);
for (i in votersFoldersNames){
voterFolderName = votersFoldersNames[i];
if (voterFolderName != null && voterFolderName != ""){
log("sendWfMessagesToVoters(), send a message to voterFolder : " + voterFolderName + "/" + INBOX_WF_VOTE_NAME);
sendMsg("vote_for_" + docToVote.name + ".html", "msgVote.html", voterFolderName + "/" + INBOX_WF_VOTE_NAME);
}
}
}
/*
At the end of the workflow process, sends a WF message to the user that initiated the current workflow process
Attention : I presume the homeFolders are grand children of the "company_home" folder (TO REVIEW).
*/
function sendWfFinishedMessageToAuthor(docToVote, wfNode){
var targetFolderPath;
targetFolderPath = args["votedDocPath"].replace("/Company Home/","").replace("/toVote","/" + INBOX_WF_VOTE_NAME);
log("sendWfFinishedMessageToAuthor(), send a message to author in folder :" + targetFolderPath);
wfData = wfNode;
sendMsg("result_of_vote_for_" + docToVote.name + ".html", "msgVoteResult.html", targetFolderPath);
}
/*
Create object to store information about a new vote workflow instance
*/
function createWfManagementData(docToVote){
var targetFolder;
var wfNode;
targetFolder = companyhome.childByNamePath(WF_DATA_FOLDER_PATH);
if (targetFolder != null) {
wfNode = targetFolder.createNode("wf_" + docToVote.name , "vote:vote_wf");
if (wfNode!=null){
wfNode.properties["vote:status"]= "inProgress";
wfNode.properties["vote:voters"]= getVotersNames(docToVote).join(",");
wfNode.properties["vote:refusers"]= "";
wfNode.properties["vote:accepters"]= "";
wfNode.properties["vote:votersWithoutOpinion"]= "";
wfNode.save();
} else {
log("createWfManagementData() - ERROR : fail to create object to store info about new WF instance");
}
}
}
/*
Treat a document coming from the editor to the "toVote" folder.
*/
function treatIncomingVotableDocFromEditor(){
log("treatIncomingVotableDocFromEditor(), ==== BEGIN ===");
sendWfMessagesToVoters(document);
createWfManagementData(document);
log("treatIncomingVotableDocFromEditor(), ==== END =====");
}
function getWfDataNode(votedDocName){
var wfDataFolder;
var wfNode;
wfDataFolder = companyhome.childByNamePath(WF_DATA_FOLDER_PATH);
if (wfDataFolder != null) {
wfNode = wfDataFolder.childByNamePath("wf_" + votedDocName);
if (wfNode==null){
log("getWfDataNode() - ERROR : object where info about WF instance are stored not found");
}
}
return(wfNode);
}
/*
Calculate the new Status of the workflow
*/
function calculateNewWfStatus_BasedOnVoteOnMajority(oldStatus, votersList, acceptersList, refusersList, votersWithoutOpinionList){
var nbRefusers;
var nbAccepters;
log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : acceptersList= \"" + acceptersList + "\"");
log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : refusersList= \"" + refusersList + "\"");
nbAccepters = getNbItemsInList(acceptersList);
nbRefusers = getNbItemsInList(refusersList);
log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : nbAccepters= " + nbAccepters );
log("calculateNewWfStatus_BasedOnVoteOnMajority() - INFO : nbRefusers= " + nbRefusers );
if (nbAccepters > nbRefusers) return("accepted");
if (nbAccepters < nbRefusers) return("refused");
return("balance");
}
/*
Check if the workflow is finished
*/
function isVoteFinished_BasedOnVoteOnMajority(votersList, votersHavingVotedList, acceptersList, refusersList, votersWithoutOpinionList){
return(areListsEqual(votersList, votersHavingVotedList));
}
/*
Finish the workflow with a given status
*/
function finishWf(originalDoc, wfNode){
var targetFolderPath;
var targetFolder;
var author;
var wfStatus;
wfStatus = wfNode.properties["vote:status"];
targetFolder = originalDoc.parent.parent.childByNamePath(wfStatus);
if (targetFolder == null){
log(
"finishWf() - INFO : folder where we should move the voted document not found, a folder named " +
wfStatus +
" and localized as a brother of the folder where the voted document is was expected"
);
} else if (!targetFolder.hasPermission("Write")) {
log("finishWf() - INFO : the current user has no enough rights on the target folder :" + targetFolder.displayPath + "/" + targetFolder.name );
} else {
log("finishWf() - INFO : we move the original voted doc from " + originalDoc.displayPath + " to :" + targetFolder.displayPath + "/" + targetFolder.name );
originalDoc.move(targetFolder);
wfNode.remove();
}
}
/*
Treat decision of the voter
Decision = "accepted", "rejected", "noOpinion"
*/
function treatVoterChoice(originalDoc, wfNode, voterName, decision){
var votersList;
var acceptersList;
var refusersList;
var votersWithoutOpinionList;
var votersHavingVotedList;
var wfNodeModified = false;
var wfStatus;
var wfFinish = false;
if (wfNode == null) return(false);
votersList = wfNode.properties["vote:voters"];
acceptersList = wfNode.properties["vote:accepters"];
refusersList = wfNode.properties["vote:refusers"];
votersWithoutOpinionList = wfNode.properties["vote:votersWithoutOpinion"];
votersHavingVotedList = mergeTwoLists(acceptersList,refusersList );
votersHavingVotedList = mergeTwoLists(votersHavingVotedList,votersWithoutOpinionList );
wfStatus = wfNode.properties["vote:status"];
if ( !isInList(votersList, voterName) ){
// the voter is not in the list of voters
log("treatVoterChoice() - ERROR : the voter(\"" + voterName + "\") is not in the list of voters");
return(false);
}
if ( isInList(votersHavingVotedList, voterName) ){
// the voter has already voted
log("treatVoterChoice() - ERROR : the voter(\"" + voterName + "\") has already voted ");
return(false);
}
log("treatVoterChoice() - INFO : the voter(\"" + voterName + "\") gives his vote : \"" + decision + "\"");
switch("" + decision){
case "accepted":
log("treatVoterChoice() - INFO : treat accepted vote");
acceptersList = addItemToList(acceptersList, voterName);
wfNode.properties["vote:accepters"] = acceptersList;
wfNodeModified = true;
break;
case "rejected":
log("treatVoterChoice() - INFO : treat rejected vote");
refusersList = addItemToList(refusersList, voterName);
wfNode.properties["vote:refusers"] = refusersList;
wfNodeModified = true;
break;
case "noOpinion":
log("treatVoterChoice() - INFO : treat noOpinion vote");
votersWithoutOpinionList = addItemToList(votersWithoutOpinionList, voterName);
wfNode.properties["vote:votersWithoutOpinion"] = votersWithoutOpinionList;
wfNodeModified = true;
break;
default:
log("treatVoterChoice() - INFO : unexpected decision");
}
if (wfNodeModified){
votersHavingVotedList = addItemToList(votersHavingVotedList, voterName);
// set new status
wfStatus = calculateNewWfStatus_BasedOnVoteOnMajority(wfStatus, votersList, acceptersList, refusersList, votersWithoutOpinionList);
log("treatVoterChoice() - INFO : new status for the workflow :" + wfStatus);
wfNode.properties["vote:status"] = wfStatus;
// save the wfNode
wfNode.save();
// check if the vote is finished
wfFinish = isVoteFinished_BasedOnVoteOnMajority(votersList, votersHavingVotedList, acceptersList, refusersList, votersWithoutOpinionList);
}
return(wfFinish);
}
/*
Treat a document coming from a voter to a folder named "accepted" or "rejected" or "noOpinion".
*/
function treatVoteByHtmlSubmit(){
log("treatVoteByHtmlSubmit(), ==== BEGIN ===");
var htmlPageToReturn = "<HTML>Your vote has been submitted</HTML>";
var docName = args["votedDocName"];
var decision = args["voteDecision"];
var voterName = person.properties["cm:userName"];
var docPath = args["votedDocPath"];
var msgPath = args["msgPath"];
var originalDoc;
var msgFile;
var wfFinish;
docPath= docPath.replace("/Company Home","");
originalDoc = companyhome.childByNamePath(docPath + "/" + docName);
if (originalDoc == null){
log("treatVoteByHtmlSubmit(), orginal doc not found");
}
else {
wfNode = getWfDataNode(docName);
document = originalDoc;
wfFinish = treatVoterChoice(originalDoc, wfNode, voterName, decision);
if (wfFinish){
// the wf is finished, we send a notification to the initiator
log("treatVoteByHtmlSubmit() - INFO : the vote is finished");
sendWfFinishedMessageToAuthor(originalDoc, wfNode);
finishWf(originalDoc, wfNode);
}
}
msgFile = companyhome.childByNamePath(msgPath);
if (msgFile != null) msgFile.remove();
log("treatVoteByHtmlSubmit(), ==== END =====");
return(htmlPageToReturn);
}
// ***********************************************************************
// CONSTANTS
var WF_DATA_FOLDER_PATH = "test_vote_WF/wf_vote_data/ManagementData"; // path related to the companyHome folder
var HTML_TEMPLATE_FOLDER_PATH = "test_vote_WF/wf_vote_data/HtmlTemplates"; // path related to the companyHome folder
var INBOX_WF_VOTE_NAME = "INBOX_WF_VOTE";
// GLOBAL VARS
var document;
var msgPath = "";
var wfData= null; // node where data about current workflow instance are stored, used by the evaluation of the "msgVoteResult"
/*
MAIN
*/
function main(){
log("main(), ========= BEGIN ============");
if (document != null){
log("main(), called from rule action");
// execution by a simple WF action
var currentFolderName;
currentFolderName = document.parent.name;
if (currentFolderName == "toVote"){
treatIncomingVotableDocFromEditor();
} else {
log("main(), error : case not treated");
}
} else {
log("main(), called from servlet - INFO … votedDocName=" + args["votedDocName"] + ",votedDocPath=" + args["votedDocPath"]+ ",voteDecision=" + args["voteDecision"]);
// arguments = votedDocName, votedDocPath, voteDecision
return(treatVoteByHtmlSubmit());
}
log("main(), ========= END ============");
}
main();
07-24-2006 04:03 AM
04-17-2007 11:11 PM
04-18-2007 06:46 PM
Tags
Find what you came for
We want to make your experience in Hyland Connect as valuable as possible, so we put together some helpful links.