Alfresco Java-backed Web Scripts: история workflow
В этой статье я покажу на примере как писать Java-backed Web Script. Параллельно с этим я на том же примере продемонстрирую следующие пункты:
- как с помощью Java API альфрески собирать историю workflow;
- как передавать глобальные свойства альфрески в Java-backed Web Script;
- как создавать файлы в репозитории;
- как поднимать созданные файлы в новом workflow
В прошлый раз мной был представлен пример создания workflow на Activiti. Представим, что документооборот в компании, в которой работает описанный workflow, включает в себя также генерацию отчета о неисполненных поручениях и их статусе. Напишем скрипт, который будет собирать в файл отчета все еще неисполненные поручения. Скрипт запишет, в каком состоянии находится поручение, у кого на столе оно находится, а также соберем ссылки на документы каждого пакета поручений.
Каждый Java-backed Web Script должен имплементировать интерфейс org.springframework.extensions.webscripts.WebScript. В моем примере мы просто расширим класс AbstractWebScript и имплементируем один абстрактный метод execute.
Несмотря на то, что результаты исполнения нашего скрипта запишутся в файл в репозиторий альфрески, все же создадим JSON объект, который будет содержать историю, и запишем этот объект в web script response: если бы мы не генерировали файл, не поднимали бы его в workflow, мы могли бы использовать объект в дальнейшем в дашлете.
Ниже привожу код Java-backed Web Script:
package ru.ossportal.webscripts; import java.io.*; import java.util.*; import org.springframework.extensions.webscripts.AbstractWebScript; import org.springframework.extensions.webscripts.WebScriptException; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WebScriptResponse; import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest; import org.springframework.extensions.surf.RequestContext; import org.json.JSONException; import org.json.JSONObject; import org.alfresco.repo.workflow.*; import org.alfresco.service.cmr.workflow.*; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.security.AuthenticationService; import org.alfresco.repo.model.*; import org.alfresco.service.namespace.*; import org.alfresco.service.cmr.repository.*; import org.alfresco.model.ContentModel; import org.alfresco.repo.jscript.ScriptLogger; public class DirectiveHistory extends AbstractWebScript { ScriptLogger logger = new ScriptLogger(); private ServiceRegistry registry; private Repository repository; private String externalHost; private String externalPort; private String externalProtocol; // Отчеты о статусе неисполненных поручений будут лежать в CompanyHome/WFHISTORY private String WFHISTORY_FOLDER_NAME = "WFHISTORY"; private String WFHISTORY_FILE_NAME= "wfhistory"; /* Так как мы будем работать с историей workflow и репозиторием нам нужны текущие объекты ServiceRegistry и Repository */ public void setServiceRegistry (ServiceRegistry registry) { this.registry = registry; } public void setRepository (Repository repository) { this.repository = repository; } /* Параметры внешнего хоста, порта и протокола нам понадобятся для генерации внешней ссылки на документы в альфреске. Параметры мы берем из глобальных свойств альфрески, - параметры, прописанные в файле ALFRESCO_HOME/tomcat/shared/classes/alfresco-global.properties */ public void setExternalHost(String externalHost) { this.externalHost = externalHost; } public void setExternalPort(String externalPort) { this.externalPort = externalPort; } public void setExternalProtocol(String externalProtocol) { this.externalProtocol = externalProtocol; } /* Метод, находящий узел в альфреске */ protected NodeRef getNodeRef(NodeRef parent, String path) { List<String> pathElements = new ArrayList<String>(); StringTokenizer tokenizer = new StringTokenizer(path, "/"); while (tokenizer.hasMoreTokens()) { String childName = tokenizer.nextToken(); pathElements.add(childName); } NodeRef nodeRef = null; try { nodeRef = this.registry.getFileFolderService().resolveNamePath(parent, pathElements).getNodeRef(); } catch(Exception fnfe) {} return nodeRef; } //Метод, генерящий список внешних ссылок на документы из пакета protected String getURLString(NodeRef pkg) { NodeService nodeService = this.registry.getNodeService(); List<ChildAssociationRef> children = nodeService.getChildAssocs(pkg); String urls = ""; for (ChildAssociationRef childAssoc : children) { NodeRef child = childAssoc.getChildRef(); String name = (String) nodeService.getProperty(child, ContentModel.PROP_NAME); StringBuilder serverPathBuilder = new StringBuilder(); serverPathBuilder.append((externalProtocol!=null) ? externalProtocol : "http"); serverPathBuilder.append( "://" ); serverPathBuilder.append((externalHost!= null) ? externalHost : "localhost" ); serverPathBuilder.append((externalPort!= null) ? ":"+externalPort : ":8080" ); urls += serverPathBuilder.toString()+"/alfresco/d/d/workspace/SpacesStore/"+child.getId()+"/"+name+"; "; } return urls; } /* Создаем файл с отчетом в репозитории альфрески В данном случае создается простой текстовый файл. Вы можете переписать часть кода, чтобы сформировать тот формат файла, который вам нужен, используя разные сторонние библиотеки */ protected NodeRef createHistory(Hashtable history) { NodeRef historyNode = null; QName contentQName = QName.createQName("{http://www.alfresco.org/model/content/1.0}content"); java.text.DateFormat dateFormat = new java.text.SimpleDateFormat("dd-MMM-yyyy-HH-mm-ss"); Date d = new Date(); String date = dateFormat.format(d); String filename = WFHISTORY_FILE_NAME+date+".txt"; NodeRef companyHomeRef = this.repository.getCompanyHome(); NodeRef nodeRef = getNodeRef(companyHomeRef, WFHISTORY_FOLDER_NAME); if (nodeRef!=null) { NodeRef fnode = getNodeRef(nodeRef, filename); if (fnode!=null) { if (logger.isLoggingEnabled()) logger.warn(filename+" is already created"); this.registry.getFileFolderService().delete(fnode); } try { historyNode = this.registry.getFileFolderService().create(nodeRef, filename, contentQName).getNodeRef(); if (logger.isLoggingEnabled()) logger.info(filename+"' was created"); ContentWriter writer = this.registry.getFileFolderService().getWriter(historyNode); String total = ""; java.util.Enumeration e = history.keys(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); String[] value = (String[]) history.get(key); total +="nCтатус поручения: "+value[1]; total +="tДокументы: "+value[0]+"n"; } writer.putContent(total); } catch(Exception fee) { if (logger.isLoggingEnabled()) logger.error(filename+"' cannot be created"+fee.toString()); throw new WebScriptException(filename+"' cannot be created"+fee.toString()); } } else throw new WebScriptException("Unable to locate "+WFHISTORY_FOLDER_NAME+" path"); return historyNode; } public void execute(WebScriptRequest req, WebScriptResponse res) throws IOException { //Принимаем параметр user, представляющий из себя alfresco user id String controller = req.getParameter("user"); Hashtable history = new Hashtable(); WorkflowService wsi = this.registry.getWorkflowService(); try { //Создаем объект JSONObject, который и запишем в rsponse скрипта JSONObject obj = new JSONObject(); org.alfresco.service.namespace.QName inspection = org.alfresco.service.namespace.QName.createQName( "http://www.somecompany.ru/model/workflow/1.0", "inspection"); org.alfresco.service.namespace.QName perform = org.alfresco.service.namespace.QName.createQName( "http://www.somecompany.ru/model/workflow/1.0", "perform"); org.alfresco.service.namespace.QName approve = org.alfresco.service.namespace.QName.createQName( "http://www.somecompany.ru/model/workflow/1.0", "approve"); org.alfresco.service.namespace.QName owner = org.alfresco.service.namespace.QName.createQName( "http://www.alfresco.org/model/content/1.0", "owner"); /* Ищем все активные задачи ознакомления с поручениями. */ WorkflowTaskQuery workflowTaskQuery = new WorkflowTaskQuery(); workflowTaskQuery.setWorkflowDefinitionName("activiti$PerformDirective"); workflowTaskQuery.setActive(null); workflowTaskQuery.setTaskName(inspection); workflowTaskQuery.setTaskState(WorkflowTaskState.IN_PROGRESS); List<WorkflowTask> tasks = wsi.queryTasks(workflowTaskQuery); for (WorkflowTask task : tasks) { Map props = task.getProperties(); NodeRef node = (NodeRef)task.properties.get(WorkflowModel.ASSOC_PACKAGE); String[] temp = new String[2]; if (node!=null) temp[0] = getURLString(node); else temp[0] ="Документы отсутствуют"; temp[1] = "Поручение на ознакомлении у "+props.get(owner)+""; history.put(task.getId(), temp); obj.put(task.getId(), temp); } /* Ищем все активные задачи исполнения поручений */ workflowTaskQuery = new WorkflowTaskQuery(); workflowTaskQuery.setWorkflowDefinitionName("activiti$PerformDirective"); workflowTaskQuery.setActive(null); workflowTaskQuery.setTaskName(perform); workflowTaskQuery.setTaskState(WorkflowTaskState.IN_PROGRESS); tasks = wsi.queryTasks(workflowTaskQuery); for (WorkflowTask task : tasks) { Map props = task.getProperties(); NodeRef node = (NodeRef)task.properties.get(WorkflowModel.ASSOC_PACKAGE); String[] temp = new String[2]; if (node!=null) temp[0] = getURLString(node); else temp[0] ="Документы отсутствуют"; temp[1] = "Поручение на исполнении у "+props.get(owner)+""; history.put(task.getId(), temp); obj.put(task.getId(), temp); } /* Ищем все активные задачи проверки исполнения поручений */ workflowTaskQuery = new WorkflowTaskQuery(); workflowTaskQuery.setWorkflowDefinitionName("activiti$PerformDirective"); workflowTaskQuery.setActive(null); workflowTaskQuery.setTaskName(approve); workflowTaskQuery.setTaskState(WorkflowTaskState.IN_PROGRESS); tasks = wsi.queryTasks(workflowTaskQuery); for (WorkflowTask task : tasks) { Map props = task.getProperties(); NodeRef node = (NodeRef)task.properties.get(WorkflowModel.ASSOC_PACKAGE); String[] temp = new String[2]; if (node!=null) temp[0] = getURLString(node); else temp[0] ="Документы отсутствуют"; temp[1] = "Поручение на проверке исполнения у "+props.get(owner)+""; history.put(task.getId(), temp); obj.put(task.getId(), temp); } //Записываем результат в response String jsonString = obj.toString(); res.getWriter().write(jsonString); } catch(JSONException e) { throw new WebScriptException("Unable to serialize JSON"); } //Создаем файл отчета в репозитории NodeRef file = createHistory(history); /* Поднимаем процесс RequestResult с прицепленным отчетом Владельцем (получателем) задачи с отчетом будет юзер, переданный нам в параметре user. */ if (file!=null) { String wfdefId = wsi.getDefinitionByName("activiti$RequestResult").getId(); if (wfdefId!=null) { Map<QName, Serializable> params = new HashMap<QName, Serializable>(); NodeRef pckg = wsi.createPackage(null); NodeService nodeService = this.registry.getNodeService(); nodeService.addChild(pckg, file, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName( (String) nodeService.getProperty(file, ContentModel.PROP_NAME)))); params.put(WorkflowModel.ASSOC_PACKAGE, pckg); NodeRef cu = this.registry.getPersonService().getPerson(controller); params.put(WorkflowModel.ASSOC_ASSIGNEE, cu); params.put(WorkflowModel.PROP_WORKFLOW_DESCRIPTION, "Отчет о состоянии поручений"); WorkflowPath wfPath = wsi.startWorkflow(wfdefId, params); } } } }
Компилируем наш класс, создаем jar и помещаем его в ALFRESCO_HOME/tomcat/webapps/alfresco/WEB-INF/lib
Как видите, программа поднимает workflow с названием RequestResult. Процесс этот элементарный, состоящий всего из одной задачи с прицепленным к ней документом отчета.
Теперь мы должны объявить наш DirectiveHistory в Spring bean. Для этого в ALFRESCO_HOME/tomcat/shared/classes/extension создадим файл demo-context.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'> <beans> <bean id="webscript.demo.demo.get" class="ru.ossportal.webscripts.DirectiveHistory" parent="webscript"> <property name="repository" ref="repositoryHelper" /> <property name="serviceRegistry" ref="ServiceRegistry" /> <property name="externalHost"> <value>${alfresco.externalHost}</value> </property> <property name="externalPort"> <value>${alfresco.externalPort}</value> </property> <property name="externalProtocol"> <value>${alfresco.externalProtocol}</value> </property> </bean> </beans>
Отмечу, что в объявлении в Spring bean важно указывать установленные альфреской имена аттрибутов:
- префикс webscript означает, что вы объявляете реализацию веб-сценария;
- окончание get означает, что вэб скрипт будет "общаться" через HTTP GET;
- остальное указывает имя вэб скрипта и наименование пакета.
Таким образом мы объявили следующее:
- у нас есть вэб скрипт, имплементированный классом ru.ossportal.webscripts.DirectiveHistory;
- он объявлен пакетом org.alfresco.demo;
- его имя - demo;
- он принимает HTTP GET запросы.
Также в декларации указано, что Spring bean принимает ссылки на такие объекты, как Repository и Service Registry, а также ссылки на параметры, объявленные в alfresco-global.properties
Теперь время описать дескриптор вэб скрипта, который имплементирует GET:
<webscript> <shortname>Directive workflow history</shortname> <description>Directive report</description> <url>/demo/demo?user={user}</url> <authentication>user</authentication> <format default="">argument</format> </webscript>
Дескриптор определяет метаданные регистрации для вэб скрипта: способ аутентификации, формат и пр. В нашем случае аутентификация установлена user, что означает как доступ, так и обязательную аутентификацию для любого юзеров альфрески, как только наш скрипт вызывается. Обязательная аутентификация необходима для защиты контента альфрески, так как в скрипте мы будем работать с папками и документами в репозитории.
Также наш дескриптор описывает, что параметром запроса будет имя учетной карточки в альфреске - конечного получателя отчета.
Запишем наш дескриптор в файл demo.get.desc.xml в папку ALFRESCO_HOME/tomcat/shared/classes/alfresco/templates/webscripts/demo
После того, как мы создали дескриптор, сделаем шаблон для вывода JSON. Там же, где находится дескриптор, создадим файл demo.get.json.ftl и запишем в него следующее:
${result}
Шаблон будет выводить наш JSON объект "как есть".
Теперь осталось прописать в ALFRESCO_HOME/tomcat/shared/classes/alfresco-global.properties параметры внешнего хоста, порта и протокола. В нашем случае их значения совпадают с локальными:
alfresco.externalHost=localhost
alfresco.externalProtocol=http
alfresco.externalPort=8080
Теперь рестартуем сервер, создадим в репозитории папку CompanyHome/WFHISTORY, отредактируем в Manage Space Users в свойствах папки доступ (добавим доступ EVERYONE/Сoorfinator) и вызовем наш скрипт:
http://localhost:8080/alfresco/service/demo/demo?user=ivanov
На столе у Иванова, учетная карточка которого ivanov, появится задача с прицепленным к ней отчетом
Прикрепленные файлы | Размер |
---|---|
requestresult.png | 3.67 кб |
src.zip | 11.03 кб |
Комментарии
14/02/2013 - 09:32
14/02/2013 - 09:48
Проверьте, прописаны ли переменные в alfresco-global.properties и проверьте внимательно, как Вы описали дескриптор. И еще вопрос: рестартовали ли Вы альфреско после всего?
14/02/2013 - 09:55
14/02/2013 - 10:23
14/02/2013 - 15:12
15/02/2013 - 08:04
15/02/2013 - 09:59
18/02/2013 - 09:01
18/02/2013 - 09:01
19/02/2013 - 09:59
Знаете, все же попытайтесь открыть все xml-ки - дескриптора и бинов - в каком-нить броузере или же редакторе xml-а и убедитесь, что они правильные, что все тэги закрыты и на своих местах
19/02/2013 - 10:13
19/02/2013 - 10:19
19/02/2013 - 10:22
19/02/2013 - 10:25
19/02/2013 - 14:51
20/02/2013 - 18:00
Такая ошибка возникала у меня в следующих случаях:
- когда xml-ки конфигурации были невалидными, неправильными, какая-то синтаксическая ошибка влезла в тексты;
- id бинов совпадали с какими-то другими, уже существующими;
- файлы конфигурации располагались в неправильных местах под альфреской
Я боюсь, больше ничем помочь не могу. Тут уже надо смотреть на месте.