!165 绝对路径的目录位置及其所有子目录下规则配置文件的侦听

Merge pull request !165 from 与或非/issues/I6BDLN
This commit is contained in:
铂赛东 2023-02-14 10:24:04 +00:00 committed by Gitee
commit 394136b988
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
48 changed files with 1052 additions and 58 deletions

View File

@ -7,6 +7,7 @@ import com.yomahub.liteflow.enums.NodeTypeEnum;
import com.yomahub.liteflow.exception.NodeBuildException;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.flow.element.Node;
import com.yomahub.liteflow.monitor.MonitorFile;
import com.yomahub.liteflow.spi.holder.PathContentParserHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -134,6 +135,10 @@ public class LiteFlowNodeBuilder {
List<String> scriptList = PathContentParserHolder.loadContextAware().parseContent(ListUtil.toList(filePath));
String script = CollUtil.getFirst(scriptList);
setScript(script);
// 添加脚本文件监听
List<String> fileAbsolutePath = PathContentParserHolder.loadContextAware().getFileAbsolutePath(ListUtil.toList(filePath));
MonitorFile.getInstance().addMonitorFilePaths(fileAbsolutePath);
} catch (Exception e) {
String errMsg = StrUtil.format("An exception occurred while building the node[{}],{}", this.node.getId(), e.getMessage());
throw new NodeBuildException(errMsg);

View File

@ -18,6 +18,7 @@ import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.flow.element.Chain;
import com.yomahub.liteflow.flow.element.Node;
import com.yomahub.liteflow.flow.id.IdGeneratorHolder;
import com.yomahub.liteflow.monitor.MonitorFile;
import com.yomahub.liteflow.parser.base.FlowParser;
import com.yomahub.liteflow.parser.factory.FlowParserProvider;
import com.yomahub.liteflow.parser.spi.ParserClassNameSpi;
@ -27,6 +28,7 @@ import com.yomahub.liteflow.slot.DataBus;
import com.yomahub.liteflow.slot.DefaultContext;
import com.yomahub.liteflow.slot.Slot;
import com.yomahub.liteflow.spi.holder.ContextCmpInitHolder;
import com.yomahub.liteflow.spi.holder.PathContentParserHolder;
import com.yomahub.liteflow.thread.ExecutorHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -89,11 +91,11 @@ public class FlowExecutor {
//所有的Parser的SPI实现都是以custom形式放入的且只支持xml形式
ServiceLoader<ParserClassNameSpi> loader = ServiceLoader.load(ParserClassNameSpi.class);
Iterator<ParserClassNameSpi> it = loader.iterator();
if (it.hasNext()){
if (it.hasNext()) {
ParserClassNameSpi parserClassNameSpi = it.next();
ruleSource = "el_xml:" + parserClassNameSpi.getSpiClassName();
liteflowConfig.setRuleSource(ruleSource);
}else{
} else {
//ruleSource为空而且没有spi形式的扩展那么说明真的没有ruleSource
//这种情况有可能是基于代码动态构建的
return;
@ -124,6 +126,9 @@ public class FlowExecutor {
//支持多类型的配置文件分别解析
if (BooleanUtil.isTrue(liteflowConfig.isSupportMultipleType())) {
// 添加监听文件路径
addMonitorFilePaths(ListUtil.toList(path));
// 解析文件
parser.parseMain(ListUtil.toList(path));
}
} catch (CyclicDependencyException e) {
@ -148,6 +153,9 @@ public class FlowExecutor {
//进行多个配置文件的一起解析
try {
if (parser != null) {
// 添加监听文件路径
addMonitorFilePaths(rulePathList);
// 解析文件
parser.parseMain(rulePathList);
} else {
throw new ConfigErrorException("parse error, please check liteflow config property");
@ -167,30 +175,37 @@ public class FlowExecutor {
}
//如果是ruleSource方式的最后判断下有没有解析出来,如果没有解析出来则报错
if (StrUtil.isBlank(liteflowConfig.getRuleSourceExtData()) && MapUtil.isEmpty(liteflowConfig.getRuleSourceExtDataMap())){
if (FlowBus.getChainMap().isEmpty()){
if (StrUtil.isBlank(liteflowConfig.getRuleSourceExtData()) && MapUtil.isEmpty(liteflowConfig.getRuleSourceExtDataMap())) {
if (FlowBus.getChainMap().isEmpty()) {
String errMsg = StrUtil.format("no valid rule config found in rule path [{}]", liteflowConfig.getRuleSource());
throw new ConfigErrorException(errMsg);
}
}
//执行钩子
if(hook){
if (hook) {
FlowInitHook.executeHook();
}
// 文件监听
if (liteflowConfig.getEnableMonitorFile()) {
MonitorFile.getInstance().create();
}
}
//此方法就是从原有的配置源主动拉取新的进行刷新
//和FlowBus.refreshFlowMetaData的区别就是一个为主动拉取一个为被动监听到新的内容进行刷新
public void reloadRule() {
long start = System.currentTimeMillis();
init(false);
LOG.info("reload rules takes {}ms", System.currentTimeMillis() - start);
}
//隐式流程的调用方法
@Deprecated
public void invoke(String chainId, Object param, Integer slotIndex) throws Exception {
LiteflowResponse response = this.invoke2Resp(chainId, param, slotIndex, InnerChainTypeEnum.IN_SYNC);
if (!response.isSuccess()){
if (!response.isSuccess()) {
throw response.getCause();
}
}
@ -198,7 +213,7 @@ public class FlowExecutor {
@Deprecated
public void invokeInAsync(String chainId, Object param, Integer slotIndex) throws Exception {
LiteflowResponse response = this.invoke2Resp(chainId, param, slotIndex, InnerChainTypeEnum.IN_ASYNC);
if (!response.isSuccess()){
if (!response.isSuccess()) {
throw response.getCause();
}
}
@ -240,7 +255,7 @@ public class FlowExecutor {
//调用一个流程并返回Future<LiteflowResponse>允许多上下文的传入
public Future<LiteflowResponse> execute2Future(String chainId, Object param, Class<?>... contextBeanClazzArray) {
return ExecutorHelper.loadInstance().buildMainExecutor(liteflowConfig.getMainExecutorClass()).submit(()
-> FlowExecutorHolder.loadInstance().execute2Resp(chainId, param, contextBeanClazzArray,null));
-> FlowExecutorHolder.loadInstance().execute2Resp(chainId, param, contextBeanClazzArray, null));
}
@ -251,11 +266,11 @@ public class FlowExecutor {
//调用一个流程返回默认的上下文适用于简单的调用
@Deprecated
public DefaultContext execute(String chainId, Object param) throws Exception{
public DefaultContext execute(String chainId, Object param) throws Exception {
LiteflowResponse response = this.execute2Resp(chainId, param, DefaultContext.class);
if (!response.isSuccess()){
if (!response.isSuccess()) {
throw response.getCause();
}else{
} else {
return response.getFirstContextBean();
}
}
@ -269,8 +284,8 @@ public class FlowExecutor {
}
private LiteflowResponse invoke2Resp(String chainId,
Object param,
Integer slotIndex, InnerChainTypeEnum innerChainType) {
Object param,
Integer slotIndex, InnerChainTypeEnum innerChainType) {
Slot slot = doExecute(chainId, param, null, null, slotIndex, innerChainType);
return LiteflowResponse.newInnerResponse(chainId, slot);
}
@ -288,9 +303,9 @@ public class FlowExecutor {
//如果不是隐式流程那么需要分配Slot
if (innerChainType.equals(InnerChainTypeEnum.NONE) && ObjectUtil.isNull(slotIndex)) {
//这里可以根据class分配也可以根据bean去分配
if (ArrayUtil.isNotEmpty(contextBeanClazzArray)){
if (ArrayUtil.isNotEmpty(contextBeanClazzArray)) {
slotIndex = DataBus.offerSlotByClass(ListUtil.toList(contextBeanClazzArray));
}else{
} else {
slotIndex = DataBus.offerSlotByBean(ListUtil.toList(contextBeanArray));
}
if (BooleanUtil.isTrue(liteflowConfig.getPrintExecutionLog())) {
@ -311,7 +326,7 @@ public class FlowExecutor {
//如果是隐式流程事先把subException给置空然后把隐式流程的chainId放入slot元数据中
//我知道这在多线程调用隐式流程中会有问题但是考虑到这种场景的不会多也有其他的转换方式
//所以暂且这么做以后再优化
if (!innerChainType.equals(InnerChainTypeEnum.NONE)){
if (!innerChainType.equals(InnerChainTypeEnum.NONE)) {
slot.removeSubException(chainId);
slot.addSubChain(chainId);
}
@ -326,9 +341,9 @@ public class FlowExecutor {
if (ObjectUtil.isNotNull(param)) {
if (innerChainType.equals(InnerChainTypeEnum.NONE)) {
slot.setRequestData(param);
} else if(innerChainType.equals(InnerChainTypeEnum.IN_SYNC)){
} else if (innerChainType.equals(InnerChainTypeEnum.IN_SYNC)) {
slot.setChainReqData(chainId, param);
} else if(innerChainType.equals(InnerChainTypeEnum.IN_ASYNC)){
} else if (innerChainType.equals(InnerChainTypeEnum.IN_ASYNC)) {
slot.setChainReqData2Queue(chainId, param);
}
}
@ -351,15 +366,15 @@ public class FlowExecutor {
} catch (Exception e) {
if (ObjectUtil.isNotNull(chain)) {
String errMsg = StrUtil.format("[{}]:chain[{}] execute error on slot[{}]", slot.getRequestId(), chain.getChainName(), slotIndex);
if (BooleanUtil.isTrue(liteflowConfig.getPrintExecutionLog())){
if (BooleanUtil.isTrue(liteflowConfig.getPrintExecutionLog())) {
LOG.error(errMsg, e);
}else{
} else {
LOG.error(errMsg);
}
}else{
if (BooleanUtil.isTrue(liteflowConfig.getPrintExecutionLog())){
} else {
if (BooleanUtil.isTrue(liteflowConfig.getPrintExecutionLog())) {
LOG.error(e.getMessage(), e);
}else{
} else {
LOG.error(e.getMessage());
}
}
@ -368,7 +383,7 @@ public class FlowExecutor {
//如果是隐式流程则需要设置到隐式流程的exception属性里
if (innerChainType.equals(InnerChainTypeEnum.NONE)) {
slot.setException(e);
}else{
} else {
slot.setSubException(chainId, e);
}
} finally {
@ -389,4 +404,15 @@ public class FlowExecutor {
//把liteFlowConfig设到LiteFlowGetter中去
LiteflowConfigGetter.setLiteflowConfig(liteflowConfig);
}
/**
* 添加监听文件路径
*
* @param pathList 文件路径
*/
private void addMonitorFilePaths(List<String> pathList) throws Exception {
// 添加规则文件监听
List<String> fileAbsolutePath = PathContentParserHolder.loadContextAware().getFileAbsolutePath(pathList);
MonitorFile.getInstance().addMonitorFilePaths(fileAbsolutePath);
}
}

View File

@ -0,0 +1,74 @@
package com.yomahub.liteflow.monitor;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.watch.SimpleWatcher;
import cn.hutool.core.io.watch.WatchMonitor;
import cn.hutool.core.io.watch.watchers.DelayWatcher;
import cn.hutool.core.lang.Singleton;
import com.yomahub.liteflow.core.FlowExecutorHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.util.ArrayList;
import java.util.List;
/**
* 规则文件监听器
*
* @author tangkc
*/
public class MonitorFile {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<String> PATH_LIST = new ArrayList<>();
public static MonitorFile getInstance() {
return Singleton.get(MonitorFile.class);
}
/**
* 添加监听文件路径
*
* @param filePath 文件路径
*/
public void addMonitorFilePath(String filePath) {
PATH_LIST.add(filePath);
}
/**
* 添加监听文件路径
*
* @param filePaths 文件路径
*/
public void addMonitorFilePaths(List<String> filePaths) {
PATH_LIST.addAll(filePaths);
}
/**
* 创建文件监听
*/
public void create() {
for (String filePath : CollUtil.distinct(PATH_LIST)) {
// 这里只监听两种类型文件修改和文件覆盖
WatchMonitor.createAll(filePath, new DelayWatcher(new SimpleWatcher() {
@Override
public void onModify(WatchEvent<?> event, Path currentPath) {
logger.info("file modify,filePath={}", filePath);
FlowExecutorHolder.loadInstance().reloadRule();
}
@Override
public void onOverflow(WatchEvent<?> event, Path currentPath) {
logger.info("file over flow,filePath={}", filePath);
FlowExecutorHolder.loadInstance().reloadRule();
}
// 在监听目录或文件时如果这个文件有修改操作JDK会多次触发modify方法为了解决这个问题
// 合并 500 毫秒内相同的变化
}, 500)).start();
}
}
}

View File

@ -6,6 +6,7 @@ import java.util.List;
/**
* 基于本地的json方式EL表达式解析器
*
* @author Bryan.Zhang
* @since 2.8.0
*/

View File

@ -94,6 +94,17 @@ public class LiteflowConfig {
//替补组件class路径
private String substituteCmpClass;
// 规则文件/脚本文件变更监听
private Boolean enableMonitorFile = Boolean.FALSE;
public Boolean getEnableMonitorFile() {
return enableMonitorFile;
}
public void setEnableMonitorFile(Boolean enableMonitorFile) {
this.enableMonitorFile = enableMonitorFile;
}
public Boolean getEnable() {
if (ObjectUtil.isNull(enable)) {
return Boolean.TRUE;

View File

@ -2,7 +2,23 @@ package com.yomahub.liteflow.spi;
import java.util.List;
public interface PathContentParser extends SpiPriority{
public interface PathContentParser extends SpiPriority {
/**
* 解析路径下的文件内容
*
* @param pathList 文件路径支持 classpath 路径和 file 绝对路径spring 环境支持 PathMatchingResourcePatternResolver 规则
* @return 返回文件内容
* @throws Exception ex
*/
List<String> parseContent(List<String> pathList) throws Exception;
/**
* 获取文件路径的绝对路径
*
* @param pathList 文件路径支持 classpath 路径和 file 绝对路径spring 环境支持 PathMatchingResourcePatternResolver 规则
* @return 返回文件绝对路径
* @throws Exception ex
*/
List<String> getFileAbsolutePath(List<String> pathList) throws Exception;
}

View File

@ -2,7 +2,10 @@ package com.yomahub.liteflow.spi.local;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.exception.ConfigErrorException;
import com.yomahub.liteflow.spi.PathContentParser;
@ -18,14 +21,14 @@ public class LocalPathContentParser implements PathContentParser {
@Override
public List<String> parseContent(List<String> pathList) throws Exception {
if(CollectionUtil.isEmpty(pathList)){
if (CollectionUtil.isEmpty(pathList)) {
throw new ConfigErrorException("rule source must not be null");
}
List<String> contentList = new ArrayList<>();
for(String path : pathList){
if (FileUtil.isAbsolutePath(path) && FileUtil.isFile(path)){
for (String path : pathList) {
if (FileUtil.isAbsolutePath(path) && FileUtil.isFile(path)) {
path = FILE_URL_PREFIX + path;
} else {
if (!path.startsWith(CLASSPATH_URL_PREFIX)) {
@ -33,7 +36,7 @@ public class LocalPathContentParser implements PathContentParser {
}
}
String content = ResourceUtil.readUtf8Str(path);
if (StrUtil.isNotBlank(content)){
if (StrUtil.isNotBlank(content)) {
contentList.add(content);
}
}
@ -41,6 +44,33 @@ public class LocalPathContentParser implements PathContentParser {
return contentList;
}
@Override
public List<String> getFileAbsolutePath(List<String> pathList) throws Exception {
if (CollectionUtil.isEmpty(pathList)) {
throw new ConfigErrorException("rule source must not be null");
}
List<String> result = new ArrayList<>();
for (String path : pathList) {
if (FileUtil.isAbsolutePath(path) && FileUtil.isFile(path)) {
path = FILE_URL_PREFIX + path;
result.add(new FileResource(path).getFile().getAbsolutePath());
} else {
if (!path.startsWith(CLASSPATH_URL_PREFIX)) {
path = CLASSPATH_URL_PREFIX + path;
// 这里会有自定义解析器
if(ClassLoaderUtil.isPresent(path)){
result.add(new ClassPathResource(path).getAbsolutePath());
}
}
}
}
return result;
}
@Override
public int priority() {
return 2;

View File

@ -3,6 +3,7 @@ package com.yomahub.liteflow.spi.solon;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.stream.StreamUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.exception.ConfigErrorException;
@ -10,17 +11,42 @@ import com.yomahub.liteflow.spi.PathContentParser;
import org.noear.solon.Utils;
import java.io.File;
import java.net.URI;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class SolonPathContentParser implements PathContentParser {
@Override
public List<String> parseContent(List<String> pathList) throws Exception {
if(CollectionUtil.isEmpty(pathList)){
List<URL> allResource = getUrls(pathList);
//转换成内容List
List<String> contentList = new ArrayList<>();
for (URL resource : allResource) {
String content = IoUtil.read(resource.openStream(), CharsetUtil.CHARSET_UTF_8);
if (StrUtil.isNotBlank(content)) {
contentList.add(content);
}
}
return contentList;
}
@Override
public List<String> getFileAbsolutePath(List<String> pathList) throws Exception {
List<URL> allResource = getUrls(pathList);
return StreamUtil.of(allResource)
.map(URL::getPath)
.filter(FileUtil::isFile)
.collect(Collectors.toList());
}
private static List<URL> getUrls(List<String> pathList) throws MalformedURLException {
if (CollectionUtil.isEmpty(pathList)) {
throw new ConfigErrorException("rule source must not be null");
}
@ -34,27 +60,19 @@ public class SolonPathContentParser implements PathContentParser {
path = path.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length());
}
allResource.add(Utils.getResource(path));
if (Utils.getResource(path) != null) {
allResource.add(Utils.getResource(path));
}
}
}
//如果有多个资源检查资源都是同一个类型如果出现不同类型的配置则抛出错误提示
Set<String> fileTypeSet = new HashSet<>();
allResource.forEach(resource -> fileTypeSet.add(FileUtil.extName(resource.getPath())));
if (fileTypeSet.size() != 1) {
if (fileTypeSet.size() > 1) {
throw new ConfigErrorException("config error,please use the same type of configuration");
}
//转换成内容List
List<String> contentList = new ArrayList<>();
for (URL resource : allResource) {
String content = IoUtil.read(resource.openStream(), CharsetUtil.CHARSET_UTF_8);
if (StrUtil.isNotBlank(content)){
contentList.add(content);
}
}
return contentList;
return allResource;
}
@Override

View File

@ -70,6 +70,17 @@ public class LiteflowProperty {
//替补组件的class路径
private String substituteCmpClass;
// 规则文件/脚本文件变更监听
private Boolean enableMonitorFile;
public Boolean getEnableMonitorFile() {
return enableMonitorFile;
}
public void setEnableMonitorFile(Boolean enableMonitorFile) {
this.enableMonitorFile = enableMonitorFile;
}
public boolean isEnable() {
return enable;
}

View File

@ -47,6 +47,7 @@ public class LiteflowPropertyAutoConfiguration {
liteflowConfig.setMainExecutorClass(property.getMainExecutorClass());
liteflowConfig.setPrintExecutionLog(property.isPrintExecutionLog());
liteflowConfig.setSubstituteCmpClass(property.getSubstituteCmpClass());
liteflowConfig.setEnableMonitorFile(property.getEnableMonitorFile());
return liteflowConfig;
}
}

View File

@ -158,6 +158,13 @@
"description": "Set period time to print monitor log.",
"sourceType": "com.yomahub.liteflow.springboot.LiteflowMonitorProperty",
"defaultValue": 300000
},
{
"name": "liteflow.enable-monitor-file",
"type": "java.lang.Boolean",
"description": "Set file change monitoring.",
"sourceType": "com.yomahub.liteflow.springboot.LiteflowMonitorProperty",
"defaultValue": false
}
]
}

View File

@ -18,3 +18,4 @@ liteflow.monitor.enable-log=false
liteflow.monitor.queue-limit=200
liteflow.monitor.delay=300000
liteflow.monitor.period=300000
liteflow.enable-monitor-file=false

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.stream.StreamUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
@ -13,15 +14,49 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class SpringPathContentParser implements PathContentParser {
@Override
public List<String> parseContent(List<String> pathList) throws Exception {
if(CollectionUtil.isEmpty(pathList)){
List<Resource> allResource = getResources(pathList);
//转换成内容List
List<String> contentList = new ArrayList<>();
for (Resource resource : allResource) {
String content = IoUtil.read(resource.getInputStream(), CharsetUtil.CHARSET_UTF_8);
if (StrUtil.isNotBlank(content)) {
contentList.add(content);
}
}
return contentList;
}
@Override
public List<String> getFileAbsolutePath(List<String> pathList) throws Exception {
List<Resource> allResource = getResources(pathList);
return StreamUtil.of(allResource)
// 过滤非 file 类型 Resource
.filter(Resource::isFile)
.map(r -> {
try {
return r.getFile().getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException(e);
}
}).collect(Collectors.toList());
}
private List<Resource> getResources(List<String> pathList) throws IOException {
if (CollectionUtil.isEmpty(pathList)) {
throw new ConfigErrorException("rule source must not be null");
}
@ -30,12 +65,12 @@ public class SpringPathContentParser implements PathContentParser {
String locationPattern;
//如果path是绝对路径且这个文件存在时我们认为这是一个本地文件路径而并非classpath路径
if (FileUtil.isAbsolutePath(path) && FileUtil.isFile(path)){
if (FileUtil.isAbsolutePath(path) && FileUtil.isFile(path)) {
locationPattern = ResourceUtils.FILE_URL_PREFIX + path;
} else {
if (!path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX) && !path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) {
locationPattern = ResourceUtils.CLASSPATH_URL_PREFIX + path;
}else{
} else {
locationPattern = path;
}
}
@ -53,19 +88,10 @@ public class SpringPathContentParser implements PathContentParser {
if (fileTypeSet.size() > 1) {
throw new ConfigErrorException("config error,please use the same type of configuration");
}
//转换成内容List
List<String> contentList = new ArrayList<>();
for (Resource resource : allResource) {
String content = IoUtil.read(resource.getInputStream(), CharsetUtil.CHARSET_UTF_8);
if (StrUtil.isNotBlank(content)){
contentList.add(content);
}
}
return contentList;
return allResource;
}
@Override
public int priority() {
return 1;

View File

@ -0,0 +1,42 @@
package com.yomahub.liteflow.test.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.io.File;
@RunWith(SpringRunner.class)
@TestPropertySource(value = "classpath:/monitorFile/application.properties")
@SpringBootTest(classes = MonitorFileELDeclMultiSpringbootTest.class)
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.monitorFile.cmp"})
public class MonitorFileELDeclMultiSpringbootTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testMonitor() throws Exception{
String absolutePath = new ClassPathResource("classpath:/monitorFile/flow.el.xml").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("THEN(a, b, c);", "THEN(a, c, b);");
FileUtil.writeString(newContent,new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertEquals("a==>c==>b", response.getExecuteStepStr());
}
}

View File

@ -0,0 +1,44 @@
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import java.util.Random;
@LiteflowComponent
public class CmpConfig {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS,nodeId = "a")
public void processA(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("ACmp executed!");
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS,nodeId = "b")
public void processB(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("BCmp executed!");
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS,nodeId = "c")
public void process(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,2 @@
liteflow.rule-source=monitorFile/flow.el.xml
liteflow.enable-monitor-file=true

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>

View File

@ -0,0 +1,42 @@
package com.yomahub.liteflow.test.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.test.BaseTest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.io.File;
@RunWith(SpringRunner.class)
@TestPropertySource(value = "classpath:/monitorFile/application.properties")
@SpringBootTest(classes = MonitorFileELDeclSpringbootTest.class)
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.monitorFile.cmp"})
public class MonitorFileELDeclSpringbootTest extends BaseTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testMonitor() throws Exception{
String absolutePath = new ClassPathResource("classpath:/monitorFile/flow.el.xml").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("THEN(a, b, c);", "THEN(a, c, b);");
FileUtil.writeString(newContent,new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertEquals("a==>c==>b", response.getExecuteStepStr());
}
}

View File

@ -0,0 +1,30 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("a")
public class ACmp{
@LiteflowMethod(LiteFlowMethodEnum.PROCESS)
public void process(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("ACmp executed!");
}
}

View File

@ -0,0 +1,30 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("b")
public class BCmp{
@LiteflowMethod(LiteFlowMethodEnum.PROCESS)
public void process(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,30 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("c")
public class CCmp{
@LiteflowMethod(LiteFlowMethodEnum.PROCESS)
public void process(NodeComponent bindCmp) {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("CCmp executed!");
}
}

View File

@ -0,0 +1,2 @@
liteflow.rule-source=monitorFile/flow.el.xml
liteflow.enable-monitor-file=true

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>

View File

@ -0,0 +1,42 @@
package com.yomahub.liteflow.test.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.core.FlowExecutorHolder;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.property.LiteflowConfig;
import com.yomahub.liteflow.test.BaseTest;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
public class LiteflowMonitorFileTest extends BaseTest {
private static FlowExecutor flowExecutor;
@BeforeClass
public static void init() {
LiteflowConfig config = new LiteflowConfig();
config.setRuleSource("monitorFile/flow.el.xml");
config.setEnableMonitorFile(true);
flowExecutor = FlowExecutorHolder.loadInstance(config);
}
@Test
public void testMultipleType() throws InterruptedException {
String absolutePath = new ClassPathResource("classpath:/monitorFile/flow.el.xml").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("THEN(a, b, c);", "THEN(a, c, b);");
FileUtil.writeString(newContent, new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertEquals("a==>c==>b", response.getExecuteStepStr());
}
}

View File

@ -0,0 +1,18 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
public class ACmp extends NodeComponent {
@Override
public void process() {
System.out.println("ACmp executed!");
}
}

View File

@ -0,0 +1,19 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
public class BCmp extends NodeComponent {
@Override
public void process() {
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,19 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
public class CCmp extends NodeComponent {
@Override
public void process() {
System.out.println("CCmp executed!");
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<nodes>
<node id="a" class="com.yomahub.liteflow.test.multipleType.cmp.ACmp"/>
<node id="b" class="com.yomahub.liteflow.test.multipleType.cmp.BCmp"/>
<node id="c" class="com.yomahub.liteflow.test.multipleType.cmp.CCmp"/>
</nodes>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>

View File

@ -0,0 +1,47 @@
package com.yomahub.liteflow.test.script.groovy.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.slot.DefaultContext;
import com.yomahub.liteflow.test.BaseTest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.io.File;
@RunWith(SpringRunner.class)
@TestPropertySource(value = "classpath:/monitorFile/application.properties")
@SpringBootTest(classes = MonitorFileGroovyELTest.class)
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.script.groovy.monitorFile.cmp"})
public class MonitorFileGroovyELTest extends BaseTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testMonitor() throws Exception{
String absolutePath = new ClassPathResource("classpath:/monitorFile/s1.groovy").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("a=3", "a=2");
FileUtil.writeString(newContent,new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
DefaultContext context = response.getFirstContextBean();
Assert.assertTrue(response.isSuccess());
Assert.assertEquals(Integer.valueOf(4), context.getData("s1"));
}
}

View File

@ -0,0 +1,20 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.script.groovy.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
@Component("a")
public class ACmp extends NodeComponent {
@Override
public void process() {
System.out.println("ACmp executed!");
}
}

View File

@ -0,0 +1,21 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.script.groovy.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
@Component("b")
public class BCmp extends NodeComponent {
@Override
public void process() {
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,21 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.script.groovy.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
@Component("c")
public class CCmp extends NodeComponent {
@Override
public void process() {
System.out.println("CCmp executed!");
}
}

View File

@ -0,0 +1,29 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.script.groovy.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.slot.DefaultContext;
import org.springframework.stereotype.Component;
@Component("d")
public class DCmp extends NodeComponent {
@Override
public void process() {
DefaultContext context = this.getFirstContextBean();
String key = "test";
if (context.hasData(key)){
int count = context.getData(key);
context.setData(key, ++count);
}else{
context.setData(key, 1);
}
}
}

View File

@ -0,0 +1,2 @@
liteflow.rule-source=monitorFile/flow.el.xml
liteflow.enable-monitor-file=true

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<nodes>
<node id="s1" name="普通脚本" type="script" file="monitorFile/s1.groovy"/>
</nodes>
<chain name="chain1">
THEN(a, b, c, s1);
</chain>
</flow>

View File

@ -0,0 +1,3 @@
Integer a=3
Integer b=2
defaultContext.setData("s1",a*b)

View File

@ -0,0 +1,36 @@
package com.yomahub.liteflow.test.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.noear.solon.annotation.Inject;
import org.noear.solon.test.SolonJUnit4ClassRunner;
import org.noear.solon.test.annotation.TestPropertySource;
import java.io.File;
@RunWith(SolonJUnit4ClassRunner.class)
@TestPropertySource("classpath:/monitorFile/application.properties")
public class MonitorFileELSpringbootTest {
@Inject
private FlowExecutor flowExecutor;
@Test
public void testMonitor() throws Exception{
String absolutePath = new ClassPathResource("classpath:/monitorFile/flow.el.xml").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("THEN(a, b, c);", "THEN(a, c, b);");
FileUtil.writeString(newContent,new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertEquals("a==>c==>b", response.getExecuteStepStr());
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.noear.solon.annotation.Component;
import java.util.Random;
@Component("a")
public class ACmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("ACmp executed!");
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.noear.solon.annotation.Component;
import java.util.Random;
@Component("b")
public class BCmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.noear.solon.annotation.Component;
import java.util.Random;
@Component("c")
public class CCmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("CCmp executed!");
}
}

View File

@ -0,0 +1,2 @@
liteflow.rule-source=monitorFile/flow.el.xml
liteflow.enable-monitor-file=true

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>

View File

@ -0,0 +1,43 @@
package com.yomahub.liteflow.test.monitorFile;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.CharsetUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.io.File;
@RunWith(SpringRunner.class)
@TestPropertySource(value = "classpath:/monitorFile/application.properties")
@SpringBootTest(classes = MonitorFileELSpringbootTest.class)
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.monitorFile.cmp"})
public class MonitorFileELSpringbootTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testMonitor() throws Exception{
String absolutePath = new ClassPathResource("classpath:/monitorFile/flow.el.xml").getAbsolutePath();
String content = FileUtil.readUtf8String(absolutePath);
String newContent = content.replace("THEN(a, b, c);", "THEN(a, c, b);");
FileUtil.writeString(newContent,new File(absolutePath), CharsetUtil.CHARSET_UTF_8);
Thread.sleep(1000);
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
Assert.assertEquals("a==>c==>b", response.getExecuteStepStr());
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("a")
public class ACmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("ACmp executed!");
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("b")
public class BCmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("BCmp executed!");
}
}

View File

@ -0,0 +1,28 @@
/**
* <p>Title: liteflow</p>
* <p>Description: 轻量级的组件式流程框架</p>
* @author Bryan.Zhang
* @email weenyc31@163.com
* @Date 2020/4/1
*/
package com.yomahub.liteflow.test.monitorFile.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component("c")
public class CCmp extends NodeComponent {
@Override
public void process() {
try {
Thread.sleep(new Random().nextInt(2000));
}catch (Exception e){
e.printStackTrace();
}
System.out.println("CCmp executed!");
}
}

View File

@ -0,0 +1,2 @@
liteflow.rule-source=monitorFile/flow.el.xml
liteflow.enable-monitor-file=true

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(a, b, c);
</chain>
</flow>