注意:本文中 Activiti 的版本爲 5.22,爲 5.X 系列的最後一個 RELEASE 版本。目前 Activiti 已經發展到了 7.X 版本,爲啥還用 5.X 版本,仍是存量項目的緣由。javascript
咱們的需求是將 Activiti 官網的 Demo 的流程設計器功能整合到咱們項目中,主要須要的功能是 Web 流程設計器 —— Activiti Modeler。Web 流程設計器的查找和保存功能是使用了 RestFul API 調用的,將源碼中的 editor-app
引入再定製化改造便可。除了查找和保存以外,咱們還須要查詢流程圖列表、新建流程圖、刪除流程圖等功能,但是發現這部分並無使用 RestFul API,這幾個功能是在後臺實現的,須要閱讀源碼查看這幾個功能如何實現。css
在閱讀源碼發現 Activiti 在前端 Demo 部分仍是比較混亂,先後端嚴重耦合,如今理清以下:html
Activiti Explorer的 Webapp 工程在源碼中的位置爲 modules/activiti-webapp-explorer2/src/main/webapp
,這個工程是官方Demo的核心工程。前端
比較奇怪的是這個 module 在 pom 文件中的 name 爲 Activiti - Webapp - Explorer V2
,而整個工程並無 V1 版本,並且這個 module 沒有放到 activiti-explorer
中,而是獨立開來,在 pom.xml
文件中依賴了 activiti-explorer
。java
首先添加了一個 WebConfigurer
的 listener 用來加載 Spring context:android
<!-- To load the Spring context -->
<listener>
<listener-class>org.activiti.explorer.servlet.WebConfigurer</listener-class>
</listener>
複製代碼
WebConfigurer
中配置了一個 mapping 用來相應相關的 REST 請求:git
...
dispatcherServlet.addMapping("/service/*");
...
複製代碼
第二個 listener 用來支持 Spring Beans 的 session-scoped 做用域:angularjs
<!-- To allow session-scoped beans in Spring -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
複製代碼
定義了兩個 filtergithub
第一個 filter 爲 ExplorerFilter
web
<filter>
<filter-name>UIFilter</filter-name>
<filter-class>org.activiti.explorer.filter.ExplorerFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>UIFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
複製代碼
查看 ExplorerFilter
類主要作了以下事情:
判斷請求地址是否以 "/ui"
、"/VAADIN"
、"/modeler.html"
、"/editor-app"
、"/service"
、"/diagram-viewer"
開頭:
"/ui"
"/service"
開頭
第二個 filter 爲 JSONPFilter
, 對 "/service"
開頭的請求進行過濾,處理 jsonp 的請求:
<filter>
<filter-name>JSONPFilter</filter-name>
<filter-class>org.activiti.explorer.servlet.JsonpCallbackFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>JSONPFilter</filter-name>
<url-pattern>/service/*</url-pattern>
</filter-mapping>
複製代碼
定義了名爲 Vaadin Application Servlet 的 servlet,響應 "/ui"
、"/VAADIN"
開頭的請求:
<servlet>
<servlet-name>Vaadin Application Servlet</servlet-name>
<servlet-class>org.activiti.explorer.servlet.ExplorerApplicationServlet</servlet-class>
<init-param>
<param-name>widgetset</param-name>
<param-value>org.activiti.explorer.CustomWidgetset</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Vaadin Application Servlet</servlet-name>
<url-pattern>/ui/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Vaadin Application Servlet</servlet-name>
<url-pattern>/VAADIN/*</url-pattern>
</servlet-mapping>
複製代碼
查看 ExplorerApplicationServlet
發現出現了一大段用java拼html片斷的代碼,主要做用是渲染根據瀏覽器類型渲染不一樣的css:
@Override
protected void writeAjaxPageHtmlVaadinScripts(Window window, String themeName, Application application, BufferedWriter page, String appUrl, String themeUri,
String appId, HttpServletRequest request) throws ServletException, IOException {
super.writeAjaxPageHtmlVaadinScripts(window, themeName, application, page, appUrl, themeUri, appId, request);
String browserDependentCss = "<script type=\"text/javascript\">//<![CDATA[" +
"var mobi = ['opera', 'iemobile', 'webos', 'android', 'blackberry', 'ipad', 'safari'];" +
"var midp = ['blackberry', 'symbian'];" +
"var ua = navigator.userAgent.toLowerCase();" +
"if ((ua.indexOf('midp') != -1) || (ua.indexOf('mobi') != -1) || ((ua.indexOf('ppc') != -1) && (ua.indexOf('mac') == -1)) || (ua.indexOf('webos') != -1)) {" +
" document.write('<link rel=\"stylesheet\" href=\"" + themeUri +"/allmobile.css\" type=\"text/css\" media=\"all\"/>');" +
" if (ua.indexOf('midp') != -1) {" +
" for (var i = 0; i < midp.length; i++) {" +
" if (ua.indexOf(midp[i]) != -1) {" +
" document.write('<link rel=\"stylesheet\" href=\"" + themeUri +"' + midp[i] + '.css\" type=\"text/css\"/>');" +
" }" +
" }"+
" }" +
" else {"+
" if ((ua.indexOf('mobi') != -1) || (ua.indexOf('ppc') != -1) || (ua.indexOf('webos') != -1)) {" +
" for (var i = 0; i < mobi.length; i++) {" +
" if (ua.indexOf(mobi[i]) != -1) {" +
" if ((mobi[i].indexOf('blackberry') != -1) && (ua.indexOf('6.0') != -1)) {" +
" document.write('<link rel=\"stylesheet\" href=\"" + themeUri + "' + mobi[i] + '6.0.css\" type=\"text/css\"/>');" +
" }" +
" else {" +
" document.write('<link rel=\"stylesheet\" href=\"" + themeUri + "' + mobi[i] + '.css\" type=\"text/css\"/>');" +
" }" +
" break;" +
" }" +
" }" +
" }" +
" }" +
" }" +
"if ((navigator.userAgent.indexOf('iPhone') != -1) || (navigator.userAgent.indexOf('iPad') != -1)) {" +
" document.write('<meta name=\"viewport\" content=\"width=device-width\" />');" +
"}" +
" //]]>" +
"</script>" +
"<!--[if lt IE 7]><link rel=\"stylesheet\" type=\"text/css\" href=\"" + themeUri + "/lt7.css\" /><![endif]-->";
page.write(browserDependentCss);
}
複製代碼
從下面代碼能夠看出,整個前端頁面由ExplorerApp
來渲染:
@Override
protected Class< ? extends Application> getApplicationClass() throws ClassNotFoundException {
return ExplorerApp.class;
}
@Override
protected Application getNewApplication(HttpServletRequest request) {
return (Application) applicationContext.getBean(ExplorerApp.class);
}
複製代碼
至此,終於找到了Activiti App的頁面入口 ExplorerApp
ExplorerApp
初始化顯示Login頁面:
public void init() {
setMainWindow(mainWindow);
mainWindow.showLoginPage();
}
複製代碼
頁面與類的對應關係:
LoginPage
MainLayout
ProcessDefinitionPage
EditorProcessDefinitionPage
ProcessDefinitionPage
主要關注 4 個功能的代碼:
主要查詢的代碼以下:
// DefaultProcessDefinitionFilter.java
...
public ProcessDefinitionQuery getQuery(RepositoryService repositoryService) {
return getBaseQuery(repositoryService)
.orderByProcessDefinitionName().asc()
.orderByProcessDefinitionKey().asc(); // name is not unique, so we add the order on key (so we can use it in the comparsion of ProcessDefinitionListItem)
}
...
protected ProcessDefinitionQuery getBaseQuery(RepositoryService repositoryService) {
return repositoryService
.createProcessDefinitionQuery()
.latestVersion()
.active();
}
...
複製代碼
獲取到了 ProcessDefinitionQuery
就能夠獲取 ProcessDefinition
列表:
List<ProcessDefinition> processDefinitions = processDefinitionQuery.listPage(start, count);
複製代碼
processDefinition
的結構以下:
須要注意的是,在啓動流程還增長了一個判斷 process-definition 是否認義了一個 start-form,因爲在咱們實際使用中不會使用到 start-form,因此咱們在改寫這塊功能的時候增長一個可否發佈的判斷:是否存在 start-form,若存在,則不能啓動;後面的啓動後判斷當前用戶是否存在該流程的任務功能能夠忽略。
// StartProcessInstanceClickListener.java
public void buttonClick(ClickEvent event) {
// Check if process-definition defines a start-form
StartFormData startFormData = formService.getStartFormData(processDefinition.getId());
if(startFormData != null && ((startFormData.getFormProperties() != null && !startFormData.getFormProperties().isEmpty()) || startFormData.getFormKey() != null)) {
parentPage.showStartForm(processDefinition, startFormData);
} else {
// Just start the process-instance since it has no form.
// TODO: Error handling
ProcessInstance processInstance = runtimeService.startProcessInstanceById(processDefinition.getId());
// Show notification of success
notificationManager.showInformationNotification(Messages.PROCESS_STARTED_NOTIFICATION, getProcessDisplayName(processDefinition));
// Switch to inbox page in case a task of this process was created
List<Task> loggedInUsersTasks = taskService.createTaskQuery()
.taskAssignee(ExplorerApp.get().getLoggedInUser().getId())
.processInstanceId(processInstance.getId())
.list();
if (!loggedInUsersTasks.isEmpty()) {
ExplorerApp.get().getViewManager().showInboxPage(loggedInUsersTasks.get(0).getId());
}
}
}
複製代碼
TODO:判斷是否可以啓動流程邏輯:
可否轉換爲可編輯模塊邏輯:
// ProcessDefinitionDetailPanel.java
if(((ProcessDefinitionEntity) processDefinition).isGraphicalNotationDefined() == false) {
editProcessDefinitionButton.setEnabled(false);
}
複製代碼
須要注意的是轉換須要彈窗讓用戶二次確認
轉換邏輯:
// ConvertProcessDefinitionPopupWindow.java
try {
InputStream bpmnStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
InputStreamReader in = new InputStreamReader(bpmnStream, "UTF-8");
XMLStreamReader xtr = xif.createXMLStreamReader(in);
BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xtr);
if (bpmnModel.getMainProcess() == null || bpmnModel.getMainProcess().getId() == null) {
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_FAILED,
i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMN_EXPLANATION));
} else {
if (bpmnModel.getLocationMap().isEmpty()) {
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_INVALID_BPMNDI,
i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMNDI_EXPLANATION));
} else {
BpmnJsonConverter converter = new BpmnJsonConverter();
ObjectNode modelNode = converter.convertToJson(bpmnModel);
Model modelData = repositoryService.newModel();
ObjectNode modelObjectNode = new ObjectMapper().createObjectNode();
modelObjectNode.put(MODEL_NAME, processDefinition.getName());
modelObjectNode.put(MODEL_REVISION, 1);
modelObjectNode.put(MODEL_DESCRIPTION, processDefinition.getDescription());
modelData.setMetaInfo(modelObjectNode.toString());
modelData.setName(processDefinition.getName());
repositoryService.saveModel(modelData);
repositoryService.addModelEditorSource(modelData.getId(), modelNode.toString().getBytes("utf-8"));
close();
ExplorerApp.get().getViewManager().showEditorProcessDefinitionPage(modelData.getId());
URL explorerURL = ExplorerApp.get().getURL();
URL url = new URL(explorerURL.getProtocol(), explorerURL.getHost(), explorerURL.getPort(),
explorerURL.getPath().replace("/ui", "") + "modeler.html?modelId=" + modelData.getId());
ExplorerApp.get().getMainWindow().open(new ExternalResource(url));
}
}
} catch(Exception e) {
notificationManager.showErrorNotification("error", e);
}
複製代碼
分爲兩種:
// ProcessDefinitionDiagramLayoutResource.java
@RestController
public class ProcessDefinitionDiagramLayoutResource extends BaseProcessDefinitionDiagramLayoutResource {
@RequestMapping(value="/process-definition/{processDefinitionId}/diagram-layout", method = RequestMethod.GET, produces = "application/json")
public ObjectNode getDiagram(@PathVariable String processDefinitionId) {
return getDiagramNode(null, processDefinitionId);
}
}
複製代碼
!注意獲取 processDefinition 和 deployment 的方法:
// AbstractProcessDefinitionDetailPanel.java
// Members
protected ProcessDefinition processDefinition;
protected Deployment deployment;
this.processDefinition = repositoryService.getProcessDefinition(processDefinitionId);
if(processDefinition != null) {
deployment = repositoryService.createDeploymentQuery().deploymentId(processDefinition.getDeploymentId()).singleResult();
}
複製代碼
// ProcessDefinitionInfoComponent.java
StreamResource diagram = null;
// Try generating process-image stream
if(processDefinition.getDiagramResourceName() != null) {
diagram = new ProcessDefinitionImageStreamResourceBuilder()
.buildStreamResource(processDefinition, repositoryService);
}
複製代碼
EditorProcessDefinitionPage
主要關注 8 個功能的代碼:
獲取列表方法:
List<Model> modelList = repositoryService.createModelQuery().list();
複製代碼
Model
的結構以下:
// ModelEditorJsonRestResource.java
@RequestMapping(value="/model/{modelId}/json", method = RequestMethod.GET, produces = "application/json")
public ObjectNode getEditorJson(@PathVariable String modelId) {
ObjectNode modelNode = null;
Model model = repositoryService.getModel(modelId);
if (model != null) {
try {
if (StringUtils.isNotEmpty(model.getMetaInfo())) {
modelNode = (ObjectNode) objectMapper.readTree(model.getMetaInfo());
} else {
modelNode = objectMapper.createObjectNode();
modelNode.put(MODEL_NAME, model.getName());
}
modelNode.put(MODEL_ID, model.getId());
ObjectNode editorJsonNode = (ObjectNode) objectMapper.readTree(
new String(repositoryService.getModelEditorSource(model.getId()), "utf-8"));
modelNode.put("model", editorJsonNode);
} catch (Exception e) {
LOGGER.error("Error creating model JSON", e);
throw new ActivitiException("Error creating model JSON", e);
}
}
return modelNode;
}
複製代碼
// NewModelPopupWindow.java
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode editorNode = objectMapper.createObjectNode();
editorNode.put("id", "canvas");
editorNode.put("resourceId", "canvas");
ObjectNode stencilSetNode = objectMapper.createObjectNode();
stencilSetNode.put("namespace", "http://b3mn.org/stencilset/bpmn2.0#");
editorNode.put("stencilset", stencilSetNode);
Model modelData = repositoryService.newModel();
ObjectNode modelObjectNode = objectMapper.createObjectNode();
modelObjectNode.put(MODEL_NAME, (String) nameTextField.getValue());
modelObjectNode.put(MODEL_REVISION, 1);
String description = null;
if (StringUtils.isNotEmpty((String) descriptionTextArea.getValue())) {
description = (String) descriptionTextArea.getValue();
} else {
description = "";
}
modelObjectNode.put(MODEL_DESCRIPTION, description);
modelData.setMetaInfo(modelObjectNode.toString());
modelData.setName((String) nameTextField.getValue());
repositoryService.saveModel(modelData);
repositoryService.addModelEditorSource(modelData.getId(), editorNode.toString().getBytes("utf-8"));
複製代碼
repositoryService.deleteModel(modelData.getId());
複製代碼
// EditorProcessDefinitionDetailPanel.java
protected void deployModelerModel(final ObjectNode modelNode) {
BpmnModel model = new BpmnJsonConverter().convertToBpmnModel(modelNode);
byte[] bpmnBytes = new BpmnXMLConverter().convertToXML(model);
String processName = modelData.getName() + ".bpmn20.xml";
Deployment deployment = repositoryService.createDeployment()
.name(modelData.getName())
.addString(processName, new String(bpmnBytes))
.deploy();
ExplorerApp.get().getViewManager().showDeploymentPage(deployment.getId());
}
複製代碼
參考這段代碼:
// ImportUploadReceiver.java
public class ImportUploadReceiver implements Receiver, FinishedListener, ModelDataJsonConstants {
...
protected void deployUploadedFile() {
try {
try {
if (fileName.endsWith(".bpmn20.xml") || fileName.endsWith(".bpmn")) {
validFile = true;
XMLInputFactory xif = XmlUtil.createSafeXmlInputFactory();
InputStreamReader in = new InputStreamReader(new ByteArrayInputStream(outputStream.toByteArray()), "UTF-8");
XMLStreamReader xtr = xif.createXMLStreamReader(in);
BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xtr);
if (bpmnModel.getMainProcess() == null || bpmnModel.getMainProcess().getId() == null) {
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_FAILED,
i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMN_EXPLANATION));
} else {
if (bpmnModel.getLocationMap().isEmpty()) {
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_INVALID_BPMNDI,
i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_BPMNDI_EXPLANATION));
} else {
String processName = null;
if (StringUtils.isNotEmpty(bpmnModel.getMainProcess().getName())) {
processName = bpmnModel.getMainProcess().getName();
} else {
processName = bpmnModel.getMainProcess().getId();
}
modelData = repositoryService.newModel();
ObjectNode modelObjectNode = new ObjectMapper().createObjectNode();
modelObjectNode.put(MODEL_NAME, processName);
modelObjectNode.put(MODEL_REVISION, 1);
modelData.setMetaInfo(modelObjectNode.toString());
modelData.setName(processName);
repositoryService.saveModel(modelData);
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
ObjectNode editorNode = jsonConverter.convertToJson(bpmnModel);
repositoryService.addModelEditorSource(modelData.getId(), editorNode.toString().getBytes("utf-8"));
}
}
} else {
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_INVALID_FILE,
i18nManager.getMessage(Messages.MODEL_IMPORT_INVALID_FILE_EXPLANATION));
}
} catch (Exception e) {
String errorMsg = e.getMessage().replace(System.getProperty("line.separator"), "<br/>");
notificationManager.showErrorNotification(Messages.MODEL_IMPORT_FAILED, errorMsg);
}
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
notificationManager.showErrorNotification("Server-side error", e.getMessage());
}
}
}
}
protected void showUploadedDeployment() {
viewManager.showEditorProcessDefinitionPage(modelData.getId());
}
}
複製代碼
// EditorProcessDefinitionDetailPanel.java
protected void exportModel() {
final FileResource stream = new FileResource(new File(""), ExplorerApp.get()) {
private static final long serialVersionUID = 1L;
@Override
public DownloadStream getStream() {
DownloadStream ds = null;
try {
byte[] bpmnBytes = null;
String filename = null;
if (SimpleTableEditorConstants.TABLE_EDITOR_CATEGORY.equals(modelData.getCategory())) {
WorkflowDefinition workflowDefinition = ExplorerApp.get().getSimpleWorkflowJsonConverter()
.readWorkflowDefinition(repositoryService.getModelEditorSource(modelData.getId()));
filename = workflowDefinition.getName();
WorkflowDefinitionConversion conversion =
ExplorerApp.get().getWorkflowDefinitionConversionFactory().createWorkflowDefinitionConversion(workflowDefinition);
bpmnBytes = conversion.getBpmn20Xml().getBytes("utf-8");
} else {
JsonNode editorNode = new ObjectMapper().readTree(repositoryService.getModelEditorSource(modelData.getId()));
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
BpmnModel model = jsonConverter.convertToBpmnModel(editorNode);
filename = model.getMainProcess().getId() + ".bpmn20.xml";
bpmnBytes = new BpmnXMLConverter().convertToXML(model);
}
ByteArrayInputStream in = new ByteArrayInputStream(bpmnBytes);
ds = new DownloadStream(in, "application/xml", filename);
// Need a file download POPUP
ds.setParameter("Content-Disposition", "attachment; filename=" + filename);
} catch(Exception e) {
LOGGER.error("failed to export model to BPMN XML", e);
ExplorerApp.get().getNotificationManager().showErrorNotification(Messages.PROCESS_TOXML_FAILED, e);
}
return ds;
}
};
stream.setCacheTime(0);
ExplorerApp.get().getMainWindow().open(stream);
}
複製代碼
// StencilsetRestResource.java
public class StencilsetRestResource {
@RequestMapping(value="/editor/stencilset", method = RequestMethod.GET, produces = "application/json;charset=utf-8")
public @ResponseBody String getStencilset() {
InputStream stencilsetStream = this.getClass().getClassLoader().getResourceAsStream("stencilset.json");
try {
return IOUtils.toString(stencilsetStream, "utf-8");
} catch (Exception e) {
throw new ActivitiException("Error while loading stencil set", e);
}
}
}
複製代碼