業(yè)務(wù)場(chǎng)景:進(jìn)行流程開(kāi)發(fā)的時(shí)候檀训,經(jīng)常需要流程設(shè)計(jì)器進(jìn)行Bpmn.xml設(shè)計(jì),flowable官方也只是提供了war下載享言,并且需要基于本身的用戶(hù)權(quán)限峻凫,這對(duì)于已有系統(tǒng)的集成是不方便的,所以對(duì)于流程設(shè)計(jì)器的權(quán)限模塊剝離就很有必要览露,本文描述了如何對(duì)流程設(shè)計(jì)器的剝離以及核心模塊的功能分析荧琼。
環(huán)境:
springboot:2.2.0.RELEASE
flowable:6.4.2git地址:https://github.com/oldguys/flowable-modeler-demo.git
flowable 官方git地址:https://github.com/flowable/flowable-engine/releases
下一篇:《flowable流程設(shè)計(jì)器集成,整合mybatis-plus差牛,從modeler 中 部署流程定義 》
本地啟動(dòng)后測(cè)試路徑:http://localhost:8081/flowable-modeler/#/processes
流程設(shè)計(jì)器抽離demo
- pom文件
- 靜態(tài)資源及配置文件
- 重寫(xiě) flowable-modeler 默認(rèn)的類(lèi)加載
- 重寫(xiě) spring-security 相關(guān)模塊
Step 1:pom 文件
flowable-modeler 模塊:從官方源碼可以分析得出命锄,基本的modeler模塊的相關(guān)引用 此處 版本是 flowable 6.4.2
mysql數(shù)據(jù)源:默認(rèn)是h2數(shù)據(jù)庫(kù),此處引用的是mysql數(shù)據(jù)庫(kù)偏化。
<!-- flowable-modeler 核心 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-conf</artifactId>
<version>${flowable-version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-rest</artifactId>
<version>${flowable-version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-logic</artifactId>
<version>${flowable-version}</version>
</dependency>
<!-- 替換數(shù)據(jù)源 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
Step 2: 靜態(tài)資源及配置文件
- 復(fù)制相關(guān)的靜態(tài)資源及配置文件
在源碼中可以看到脐恩,這些文件為前端頁(yè)面的靜態(tài)頁(yè)面,需要完成轉(zhuǎn)移到剝離出來(lái)的新項(xiàng)目之中侦讨。
2.編寫(xiě)項(xiàng)目的自定義配置文件驶冒,及springboot本身項(xiàng)目的 application.yml 文件。
logging:
level:
org.flowable.ui.modeler.rest.app: debug
# root: debug
server:
port: 8081
spring:
datasource:
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/flowable-modeler-demo?characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
PS: 由于springboot 項(xiàng)目的配置文件是逐層覆蓋的韵卤,最外層項(xiàng)目的變量 可以直接覆蓋掉原始的配置變量骗污,所以可以直接在此處 配置數(shù)據(jù)源遍可以替換掉內(nèi)部的默認(rèn)數(shù)據(jù)源變量。
Step 3:重寫(xiě) flowable-modeler 默認(rèn)的類(lèi)加載
從源碼中看到入口為2個(gè)類(lèi)沈条,本處需要對(duì)org.flowable.ui.modeler.conf.ApplicationConfiguration 進(jìn)行相應(yīng)的改造需忿。
剔除不需要的相關(guān)依賴(lài),添加缺少的依賴(lài),改造結(jié)果如下:
- 抽出自定義需要的全局配置類(lèi)
package com.example.oldguy.modules.modeler.configurations;
import org.flowable.ui.modeler.conf.DatabaseConfiguration;
import org.flowable.ui.modeler.properties.FlowableModelerAppProperties;
import org.flowable.ui.modeler.servlet.ApiDispatcherServletConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Import(
value = {
DatabaseConfiguration.class,
}
)
@Configuration
@EnableConfigurationProperties(FlowableModelerAppProperties.class)
@ComponentScan(basePackages = {
"org.flowable.ui.modeler.repository",
"org.flowable.ui.modeler.service",
"org.flowable.ui.common.repository",
"org.flowable.ui.common.tenant",
"org.flowable.ui.modeler.rest.app",
"org.flowable.ui.modeler.rest.api"
}
)
public class MyAppConfiguration {
@Bean
public ServletRegistrationBean modelerApiServlet(ApplicationContext applicationContext) {
AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
dispatcherServletConfiguration.setParent(applicationContext);
dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class);
DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration);
ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/api/*");
registrationBean.setName("Flowable Modeler App API Servlet");
registrationBean.setLoadOnStartup(1);
registrationBean.setAsyncSupported(true);
return registrationBean;
}
}
PS: org.flowable.ui.modeler.conf.DatabaseConfiguration 為flowable源碼中的mybatis配置屋厘,需要引入汞扎。
- 重寫(xiě)類(lèi) org.flowable.ui.common.rest.idm.remote.RemoteAccountResource,這個(gè)類(lèi)是流程設(shè)計(jì)器獲取用戶(hù)相關(guān)信息的接口擅这,此處的處理方式是澈魄,在新建項(xiàng)目中編寫(xiě)相同的restful接口覆蓋,然后不加載原始的類(lèi)仲翎。
package com.example.oldguy.modules.modeler.controllers;
import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.model.UserRepresentation;
import org.flowable.ui.common.security.FlowableAppUser;
import org.flowable.ui.common.security.SecurityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName: RemoteAccountResource
* @Author: ren
* @Description:
* @CreateTIme: 2020/1/25 0025 下午 10:37
**/
@RestController
public class RemoteAccountResource {
@GetMapping("app/rest/account")
public UserRepresentation getAccount() {
FlowableAppUser appUser = SecurityUtils.getCurrentFlowableAppUser();
UserRepresentation userRepresentation = new UserRepresentation(appUser.getUserObject());
if (appUser.getUserObject() instanceof RemoteUser) {
RemoteUser temp = (RemoteUser) appUser.getUserObject();
userRepresentation.setPrivileges(temp.getPrivileges());
}
return userRepresentation;
}
}
- 重寫(xiě) org.flowable.ui.common.service.idm.RemoteIdmService 痹扇,此處在 flowable-ui-modeler-rest 模塊中多次依賴(lài)注入,本處項(xiàng)目暫時(shí)沒(méi)有使用到溯香,所以這里采用重寫(xiě)空實(shí)現(xiàn)鲫构。
package com.example.oldguy.modules.modeler.services;
import org.flowable.ui.common.model.RemoteGroup;
import org.flowable.ui.common.model.RemoteToken;
import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.service.idm.RemoteIdmService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @ClassName: MyRemoteServiceImpl
* @Author: ren
* @Description:
* @CreateTIme: 2020/1/25 0025 下午 11:27
**/
@Service
public class MyRemoteServiceImpl implements RemoteIdmService {
private Logger LOGGER = LoggerFactory.getLogger(MyRemoteServiceImpl.class);
@Override
public RemoteUser authenticateUser(String username, String password) {
LOGGER.debug("MyRemoteServiceImpl:authenticateUser");
return null;
}
@Override
public RemoteToken getToken(String tokenValue) {
LOGGER.debug("MyRemoteServiceImpl:getToken");
return null;
}
@Override
public RemoteUser getUser(String userId) {
LOGGER.debug("MyRemoteServiceImpl:getUser");
return null;
}
@Override
public List<RemoteUser> findUsersByNameFilter(String filter) {
LOGGER.debug("MyRemoteServiceImpl:findUsersByNameFilter");
return null;
}
@Override
public List<RemoteUser> findUsersByGroup(String groupId) {
LOGGER.debug("MyRemoteServiceImpl:findUsersByGroup");
return null;
}
@Override
public RemoteGroup getGroup(String groupId) {
LOGGER.debug("MyRemoteServiceImpl:getGroup");
return null;
}
@Override
public List<RemoteGroup> findGroupsByNameFilter(String filter) {
LOGGER.debug("MyRemoteServiceImpl:findGroupsByNameFilter");
return null;
}
}
Step 4:重寫(xiě) spring-security 相關(guān)模塊
在源碼中 org.flowable.ui.modeler.conf.SecurityConfiguration 為流程設(shè)計(jì)器的權(quán)限核心控制,所以需要 進(jìn)行改造玫坛。本處直接基于spring-security 的運(yùn)行機(jī)制重寫(xiě)相關(guān)的實(shí)現(xiàn)類(lèi)结笨,不引用原始配置的相關(guān)信息
- 自定義認(rèn)證過(guò)濾器:com.example.oldguy.modules.modeler.security.MyFilter
package com.example.oldguy.modules.modeler.security;
import org.flowable.ui.common.security.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName: MyFilter
* @Author: huangrenhao
* @Description:
* @CreateTime: 2020/1/19 0019 下午 4:04
* @Version:
**/
@Component
@WebFilter(urlPatterns = {"/app/**", "/api/**"})
public class MyFilter extends OncePerRequestFilter {
private Logger LOGGER = LoggerFactory.getLogger(MyFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (skipAuthenticationCheck(request)) {
filterChain.doFilter(request, response);
return;
}
LOGGER.debug("MyFilter:doFilterInternal:" + request.getRequestURL());
if (StringUtils.isEmpty(SecurityUtils.getCurrentUserId())) {
LOGGER.debug("MyFilter:doFilterInternal:校驗(yàn)......");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("admin", "");
SecurityContextHolder.getContext().setAuthentication(token);
} else {
LOGGER.debug("MyFilter:doFilterInternal:校驗(yàn)通過(guò).......");
}
filterChain.doFilter(request, response);
}
protected boolean skipAuthenticationCheck(HttpServletRequest request) {
return request.getRequestURI().endsWith(".css") ||
request.getRequestURI().endsWith(".js") ||
request.getRequestURI().endsWith(".html") ||
request.getRequestURI().endsWith(".map") ||
request.getRequestURI().endsWith(".woff") ||
request.getRequestURI().endsWith(".png") ||
request.getRequestURI().endsWith(".jpg") ||
request.getRequestURI().endsWith(".jpeg") ||
request.getRequestURI().endsWith(".tif") ||
request.getRequestURI().endsWith(".tiff");
}
}
PS: 經(jīng)過(guò)試驗(yàn),過(guò)濾器必須繼承于 org.springframework.web.filter.OncePerRequestFilter 而不能直接實(shí)現(xiàn) javax.servlet.Filter 湿镀,不然就算注入到 spring-security容器中炕吸,也不能觸發(fā)本身的權(quán)限校驗(yàn),具體原理還有待研究勉痴。此處參考源碼中的 org.flowable.ui.common.filter.FlowableCookieFilter進(jìn)行改造
- 編寫(xiě)核心的校驗(yàn)類(lèi) com.example.oldguy.modules.modeler.security.MyUserDetailsService
package com.example.oldguy.modules.modeler.security;
import org.flowable.ui.common.model.RemoteUser;
import org.flowable.ui.common.security.FlowableAppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import static com.example.oldguy.modules.modeler.constants.FlowableConstants.FLOW_ABLE_MODELER_ROLES;
/**
* @ClassName: MyUserDetailsService
* @Author: huangrenhao
* @Description:
* @CreateTime: 2020/1/17 0017 下午 4:37
* @Version:
**/
@Service
public class MyUserDetailsService implements UserDetailsService {
private Logger LOGGER = LoggerFactory.getLogger(MyUserDetailsService.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LOGGER.debug("MyUserDetailsService:loadUserByUsername:認(rèn)證權(quán)限.....");
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
// 配置 flowable-modeler 權(quán)限
FLOW_ABLE_MODELER_ROLES.parallelStream().forEach(obj -> {
authorities.add(new SimpleGrantedAuthority(obj));
});
RemoteUser sourceUser = new RemoteUser();
sourceUser.setFirstName("admin");
sourceUser.setDisplayName("測(cè)試中文");
sourceUser.setPassword("123456");
sourceUser.setPrivileges(new ArrayList<>(FLOW_ABLE_MODELER_ROLES));
sourceUser.setId("123456");
FlowableAppUser user = new FlowableAppUser(sourceUser, "admin", authorities);
return user;
}
}
- 編寫(xiě)密碼編譯器 com.example.oldguy.modules.modeler.security.MyPasswordEncoder
package com.example.oldguy.modules.modeler.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @ClassName: MyPasswordEncoder
* @Author: ren
* @Description:
* @CreateTIme: 2020/1/25 0025 下午 7:11
**/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
private Logger LOGGER = LoggerFactory.getLogger(MyPasswordEncoder.class);
@Override
public String encode(CharSequence charSequence) {
LOGGER.debug("MyPasswordEncoder:encode:" + charSequence);
return charSequence.toString();
}
@Override
public boolean matches(CharSequence frontPsw, String sourcePsw) {
LOGGER.debug("MyPasswordEncoder:matches:" + frontPsw + "\t sourcePsw:" + sourcePsw);
return true;
}
}
- 編寫(xiě)spring-security 核心容器
package com.example.oldguy.modules.modeler.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;
/**
* @ClassName: WebSecurityConfig
* @Author: huangrenhao
* @Description:
* @CreateTime: 2020/1/19 0019 下午 3:16
* @Version:
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilter myFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(myFilter, SecurityContextPersistenceFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and().csrf().disable();
}
}
PS: csrf 要關(guān)掉赫模,不然前端調(diào)用會(huì)被攔截。出現(xiàn) Forbidden 一閃而過(guò)的情況蒸矛。
以上就完成流程設(shè)計(jì)器抽離瀑罗。
下面是對(duì)于其中的部分模塊及原理的解析:
- org.flowable.ui.common.security.SecurityUtils 用戶(hù)信息
- 模型設(shè)計(jì)器配置相應(yīng)模塊
- spring-security 過(guò)濾鏈
1. org.flowable.ui.common.security.SecurityUtils 用戶(hù)信息
// org.flowable.ui.common.security.SecurityUtils
public static FlowableAppUser getCurrentFlowableAppUser() {
FlowableAppUser user = null;
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext != null && securityContext.getAuthentication() != null) {
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof FlowableAppUser) {
user = (FlowableAppUser) principal;
}
}
return user;
}
// com.example.oldguy.modules.modeler.security.MyUserDetailsService
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LOGGER.debug("MyUserDetailsService:loadUserByUsername:認(rèn)證權(quán)限.....");
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
// 配置 flowable-modeler 權(quán)限
FLOW_ABLE_MODELER_ROLES.parallelStream().forEach(obj -> {
authorities.add(new SimpleGrantedAuthority(obj));
});
RemoteUser sourceUser = new RemoteUser();
sourceUser.setFirstName("admin");
sourceUser.setDisplayName("測(cè)試中文");
sourceUser.setPassword("123456");
sourceUser.setPrivileges(new ArrayList<>(FLOW_ABLE_MODELER_ROLES));
sourceUser.setId("123456");
FlowableAppUser user = new FlowableAppUser(sourceUser, "admin", authorities);
return user;
}
PS: 從官網(wǎng)中可以看到 用戶(hù)是基于 ThreadLocal 的,所以是線(xiàn)程安全的雏掠,并且在 SecurityUtils 調(diào)用的時(shí)候 FlowableAppUser 才會(huì)返回斩祭,所以在重寫(xiě) org.springframework.security.core.userdetails.UserDetailsService 實(shí)現(xiàn)類(lèi)的時(shí)候,需要使用 FlowableAppUser 作為用戶(hù)類(lèi)乡话。
另外摧玫,restful接口:http://localhost:8888/flowable-modeler/app/rest/account
默認(rèn)返回值:
{
"id": "admin",
"firstName": "Test",
"lastName": "Administrator",
"email": "admin@flowable.org",
"fullName": "Test Administrator",
"tenantId": null,
"groups": [],
"privileges": ["access-idm", "access-rest-api", "access-task", "access-modeler", "access-admin"]
}
其中:["access-idm", "access-rest-api", "access-task", "access-modeler", "access-admin"]
分別對(duì)應(yīng)流程設(shè)計(jì)器導(dǎo)航欄上面的各個(gè)功能,可以根據(jù)需要進(jìn)行刪減改造
2. 模型設(shè)計(jì)器配置相應(yīng)模塊
模型設(shè)計(jì)器中功能特別多蚊伞,并不是所有功能都需要用到的席赂,可以通過(guò)修改配置文件進(jìn)行修改
資源位置:
stencilset_bpmn.json(流程設(shè)計(jì)器界面配置文件):D:\workspace\demo\flowable\flowable-engine-flowable-6.4.2-old\modules\flowable-ui-modeler\flowable-ui-modeler-logic\src\main\resources\stencilset_bpmn.json
StencilSetResource(流程設(shè)計(jì)器配置后端接口): org.flowable.ui.modeler.rest.app.StencilSetResource
zh-CN.json(可參考的漢化文件):static/i18n/zh-CN.json
3. spring-security 過(guò)濾鏈
spring-security的過(guò)濾鏈 基于 org.springframework.security.config.annotation.web.builders.FilterComparator
debug的時(shí)候可以拿到數(shù)據(jù)(如下),所以配置過(guò)濾器的時(shí)候时迫,必須優(yōu)先于指定的節(jié)點(diǎn)颅停,如LogoutFilter,如:CorsFilter=600掠拳,如果后于此過(guò)濾器癞揉,會(huì)出現(xiàn)沒(méi)有權(quán)限,然后界面一閃而過(guò)。很難調(diào)試喊熟,所以需要通過(guò)過(guò)濾鏈順序?qū)τ谥付ǖ臋?quán)限攔截進(jìn)行處理柏肪。
{
org.springframework.security.openid.OpenIDAuthenticationFilter=1600,
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter=1300,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter=1800,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter=900,
org.springframework.security.web.context.SecurityContextPersistenceFilter=400,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor=3100,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter=1700,
org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter=1000,
org.springframework.security.web.session.SessionManagementFilter=2900,
org.springframework.security.web.authentication.logout.LogoutFilter=800,
org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter=1100,
org.springframework.security.web.jaasapi.JaasApiIntegrationFilter=2500,
org.springframework.security.web.authentication.switchuser.SwitchUserFilter=3200,
org.springframework.security.web.access.channel.ChannelProcessingFilter=100,
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter=2600,
org.springframework.security.web.session.ConcurrentSessionFilter=1900,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter=2200,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter=2700,
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter=2100,
org.springframework.security.web.csrf.CsrfFilter=700,
org.springframework.security.cas.web.CasAuthenticationFilter=1200,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter=2400,
org.springframework.security.web.access.ExceptionTranslationFilter=3000,
org.springframework.security.web.authentication.www.DigestAuthenticationFilter=2000,
org.springframework.web.filter.CorsFilter=600,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter=2300,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter=2800,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter=1400,
org.springframework.security.web.header.HeaderWriterFilter=500,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter=300
}
獲取已保存的 bpmn.xml
源碼:org.flowable.ui.modeler.rest.app.ModelBpmnResource
調(diào)用接口:127.0.0.1:8081/flowable-modeler/app/rest/models/9f545d3b-40ac-11ea-9aaf-283a4d3b99a3/bpmn20
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.flowable.org/processdef">
<process id="test" name="test" isExecutable="true">
<startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
<userTask id="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" name="測(cè)試" flowable:formFieldValidation="true"></userTask>
<sequenceFlow id="sid-8E539945-3C77-4630-90D1-984D585F8AE9" sourceRef="startEvent1" targetRef="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E"></sequenceFlow>
<endEvent id="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F"></endEvent>
<sequenceFlow id="sid-64160218-43FF-4F5A-B51A-8C11213ACC8D" sourceRef="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" targetRef="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_test">
<bpmndi:BPMNPlane bpmnElement="test" id="BPMNPlane_test">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="100.0" y="163.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E" id="BPMNShape_sid-AF5CC33E-0CE1-49EE-AEF6-6473F785A10E">
<omgdc:Bounds height="80.0" width="100.0" x="214.39999999999998" y="144.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-C633FA15-0F2F-4739-B576-EE0C2072B40F" id="BPMNShape_sid-C633FA15-0F2F-4739-B576-EE0C2072B40F">
<omgdc:Bounds height="28.0" width="28.0" x="407.4" y="170.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-8E539945-3C77-4630-90D1-984D585F8AE9" id="BPMNEdge_sid-8E539945-3C77-4630-90D1-984D585F8AE9">
<omgdi:waypoint x="129.93799288040108" y="178.5999272073778"></omgdi:waypoint>
<omgdi:waypoint x="214.39999999999998" y="181.99196787148597"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-64160218-43FF-4F5A-B51A-8C11213ACC8D" id="BPMNEdge_sid-64160218-43FF-4F5A-B51A-8C11213ACC8D">
<omgdi:waypoint x="314.35" y="184.0"></omgdi:waypoint>
<omgdi:waypoint x="407.4" y="184.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>