spider-flow

master
MaNing 3 years ago
parent 7deae2d660
commit 9261136829

3
.gitattributes vendored

@ -0,0 +1,3 @@
*.js linguist-language=java
*.css linguist-language=java
*.html linguist-language=java

9
.gitignore vendored

@ -0,0 +1,9 @@
target
*.iml
out/
.idea
.classpath
.project
.settings
bin/
.myeclipse

@ -0,0 +1,13 @@
FROM java:8
MAINTAINER octopus
RUN mkdir -p /spider-flow
WORKDIR /spider-flow
EXPOSE 8088
ADD ./spider-flow-web/target/spider-flow.jar ./
CMD sleep 30;java -Djava.security.egd=file:/dev/./urandom -jar spider-flow.jar

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 小东
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,2 +1,59 @@
# spider-flow
<p align="center">
<img src="https://www.spiderflow.org/images/logo.svg" width="600">
</p>
<p align="center">
<a target="_blank" href="https://www.oracle.com/technetwork/java/javase/downloads/index.html"><img src="https://img.shields.io/badge/JDK-1.8+-green.svg" /></a>
<a target="_blank" href="https://www.spiderflow.org"><img src="https://img.shields.io/badge/Docs-latest-blue.svg"/></a>
<a target="_blank" href="https://github.com/ssssssss-team/spider-flow/releases"><img src="https://img.shields.io/github/v/release/ssssssss-team/spider-flow?logo=github"></a>
<a target="_blank" href='https://gitee.com/ssssssss-team/spider-flow'><img src="https://gitee.com/ssssssss-team/spider-flow/badge/star.svg?theme=white" /></a>
<a target="_blank" href='https://github.com/ssssssss-team/spider-flow'><img src="https://img.shields.io/github/stars/ssssssss-team/spider-flow.svg?style=social"/></a>
<a target="_blank" href="LICENSE"><img src="https://img.shields.io/:license-MIT-blue.svg"></a>
<a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=10faa4cf9743e0aa379a72f2ad12a9e576c81462742143c8f3391b52e8c3ed8d"><img src="https://img.shields.io/badge/Join-QQGroup-blue"></a>
</p>
[介绍](#介绍) | [特性](#特性) | [插件](#插件) | <a target="_blank" href="http://demo.spiderflow.org">DEMO站点</a> | <a target="_blank" href="https://www.spiderflow.org">文档</a> | <a target="_blank" href="https://www.spiderflow.org/changelog.html">更新日志</a> | [截图](#项目部分截图) | [其它开源](#其它开源项目) | [免责声明](#免责声明)
## 介绍
平台以流程图的方式定义爬虫,是一个高度灵活可配置的爬虫平台
## 特性
- [x] 支持Xpath/JsonPath/css选择器/正则提取/混搭提取
- [x] 支持JSON/XML/二进制格式
- [x] 支持多数据源、SQL select/selectInt/selectOne/insert/update/delete
- [x] 支持爬取JS动态渲染(或ajax)的页面
- [x] 支持代理
- [x] 支持自动保存至数据库/文件
- [x] 常用字符串、日期、文件、加解密等函数
- [x] 支持插件扩展(自定义执行器,自定义方法)
- [x] 任务监控,任务日志
- [x] 支持HTTP接口
- [x] 支持Cookie自动管理
- [x] 支持自定义函数
## 插件
- [x] [Selenium插件](https://gitee.com/ssssssss-team/spider-flow-selenium)
- [x] [Redis插件](https://gitee.com/ssssssss-team/spider-flow-redis)
- [x] [OSS插件](https://gitee.com/ssssssss-team/spider-flow-oss)
- [x] [Mongodb插件](https://gitee.com/ssssssss-team/spider-flow-mongodb)
- [x] [IP代理池插件](https://gitee.com/ssssssss-team/spider-flow-proxypool)
- [x] [OCR识别插件](https://gitee.com/ssssssss-team/spider-flow-ocr)
- [x] [电子邮箱插件](https://gitee.com/ssssssss-team/spider-flow-mailbox)
## 项目部分截图
### 爬虫列表
![爬虫列表](https://images.gitee.com/uploads/images/2020/0412/104521_e1eb3fbb_297689.png "list.png")
### 爬虫测试
![爬虫测试](https://images.gitee.com/uploads/images/2020/0412/104659_b06dfbf0_297689.gif "test.gif")
### Debug
![Debug](https://images.gitee.com/uploads/images/2020/0412/104741_f9e1190e_297689.png "debug.png")
### 日志
![日志](https://images.gitee.com/uploads/images/2020/0412/104800_a757f569_297689.png "logo.png")
## 其它开源项目
- [spider-flow-vuespider-flow的前端](https://gitee.com/ssssssss-team/spider-flow-vue)
- [magic-api一个以XML为基础自动映射为HTTP接口的框架](https://gitee.com/ssssssss-team/magic-api)
- [magic-api-spring-boot-starter](https://gitee.com/ssssssss-team/magic-api-spring-boot-starter)
## 免责声明
请勿将`spider-flow`应用到任何可能会违反法律规定和道德约束的工作中,请友善使用`spider-flow`,遵守蜘蛛协议,不要将`spider-flow`用于任何非法用途。如您选择使用`spider-flow`即代表您遵守此协议,作者不承担任何由于您违反此协议带来任何的法律风险和损失,一切后果由您承担。

File diff suppressed because one or more lines are too long

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow</artifactId>
<version>0.5.0</version>
<packaging>pom</packaging>
<name>spider-flow</name>
<url>https://gitee.com/jmxd/spider-flow</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spider-flow.version>${project.version}</spider-flow.version>
<alibaba.fastjson.version>1.2.58</alibaba.fastjson.version>
<alibaba.druid.version>1.1.16</alibaba.druid.version>
<alibaba.transmittable.version>2.11.2</alibaba.transmittable.version>
<mybatis.plus.version>3.1.0</mybatis.plus.version>
<apache.commons.text.verion>1.6</apache.commons.text.verion>
<apache.commons.csv.verion>1.8</apache.commons.csv.verion>
<commons.io.version>2.6</commons.io.version>
<guava.version>28.2-jre</guava.version>
<jsoup.version>1.11.3</jsoup.version>
<xsoup.version>0.3.1</xsoup.version>
</properties>
<dependencies>
<!-- spring-boot相关配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- alibaba相关包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${alibaba.fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${alibaba.druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${alibaba.transmittable.version}</version>
</dependency>
<!-- apache commons相关 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${apache.commons.text.verion}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>${apache.commons.csv.verion}</version>
</dependency>
<!-- commons包 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- 其它包 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>xsoup</artifactId>
<version>${xsoup.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-api</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-core</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-selenium</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-proxypool</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-mongodb</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-redis</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-ocr</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-oss</artifactId>
<version>${spider-flow.version}</version>
</dependency>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-mailbox</artifactId>
<version>${spider-flow.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>spider-flow-api</module>
<module>spider-flow-core</module>
<module>spider-flow-web</module>
</modules>
</project>

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow</artifactId>
<version>0.5.0</version>
</parent>
<artifactId>spider-flow-api</artifactId>
<name>spider-flow-api</name>
<url>https://gitee.com/jmxd/spider-flow/tree/master/spider-flow-api</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

@ -0,0 +1,18 @@
package org.spiderflow;
import java.util.Map;
/**
*
*/
public interface ExpressionEngine {
/**
*
* @param expression
* @param variables
* @return
*/
Object execute(String expression, Map<String, Object> variables);
}

@ -0,0 +1,10 @@
package org.spiderflow;
import java.util.List;
import org.spiderflow.model.Grammer;
public interface Grammerable {
List<Grammer> grammers();
}

@ -0,0 +1,17 @@
package org.spiderflow.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Comment {
String value();
}

@ -0,0 +1,17 @@
package org.spiderflow.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Example {
String value();
}

@ -0,0 +1,17 @@
package org.spiderflow.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Return {
Class<?>[] value();
}

@ -0,0 +1,39 @@
package org.spiderflow.common;
import org.spiderflow.model.JsonBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
public abstract class CURDController<S extends ServiceImpl<M, T>,M extends BaseMapper<T>, T> {
@Autowired
private S service;
@RequestMapping("/list")
public IPage<T> list(@RequestParam(name = "page",defaultValue = "1")Integer page, @RequestParam(name = "limit",defaultValue = "1")Integer size){
return service.page(new Page<T>(page, size), new QueryWrapper<T>().orderByDesc("create_date"));
}
@RequestMapping("get")
public JsonBean<T> get(String id) {
return new JsonBean<T>(service.getById(id));
}
@RequestMapping("delete")
public JsonBean<Boolean> delete(String id){
return new JsonBean<Boolean>(service.removeById(id));
}
@RequestMapping("save")
public JsonBean<Boolean> save(T t){
return new JsonBean<Boolean>(service.saveOrUpdate(t));
}
}

@ -0,0 +1,46 @@
package org.spiderflow.concurrent;
import org.spiderflow.model.SpiderNode;
import java.util.Comparator;
import java.util.PriorityQueue;
public class ChildPriorThreadSubmitStrategy implements ThreadSubmitStrategy{
private Object mutex = this;
private Comparator<SpiderNode> comparator = (o1, o2) -> {
if(o1.hasLeftNode(o2.getNodeId())){
return -1;
}
return 1;
};
private PriorityQueue<SpiderFutureTask<?>> priorityQueue = new PriorityQueue<>((o1, o2) -> comparator.compare(o1.getNode(),o2.getNode()));
@Override
public Comparator<SpiderNode> comparator() {
return comparator;
}
@Override
public void add(SpiderFutureTask<?> task) {
synchronized (mutex){
priorityQueue.add(task);
}
}
@Override
public boolean isEmpty() {
synchronized (mutex){
return priorityQueue.isEmpty();
}
}
@Override
public SpiderFutureTask<?> get() {
synchronized (mutex){
return priorityQueue.poll();
}
}
}

@ -0,0 +1,32 @@
package org.spiderflow.concurrent;
import org.spiderflow.model.SpiderNode;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class LinkedThreadSubmitStrategy implements ThreadSubmitStrategy{
private List<SpiderFutureTask<?>> taskList = new CopyOnWriteArrayList<>();
@Override
public Comparator<SpiderNode> comparator() {
return (o1, o2) -> -1;
}
@Override
public void add(SpiderFutureTask<?> task) {
taskList.add(task);
}
@Override
public boolean isEmpty() {
return taskList.isEmpty();
}
@Override
public SpiderFutureTask<?> get() {
return taskList.remove(0);
}
}

@ -0,0 +1,47 @@
package org.spiderflow.concurrent;
import org.spiderflow.model.SpiderNode;
import java.util.Comparator;
import java.util.PriorityQueue;
public class ParentPriorThreadSubmitStrategy implements ThreadSubmitStrategy {
private Object mutex = this;
private Comparator<SpiderNode> comparator = (o1, o2) -> {
if (o1.hasLeftNode(o2.getNodeId())) {
return 1;
}
return -1;
};
private PriorityQueue<SpiderFutureTask<?>> priorityQueue = new PriorityQueue<>((o1, o2) -> comparator.compare(o1.getNode(), o2.getNode()));
@Override
public Comparator<SpiderNode> comparator() {
return comparator;
}
@Override
public void add(SpiderFutureTask<?> task) {
synchronized (mutex) {
priorityQueue.add(task);
}
}
@Override
public boolean isEmpty() {
synchronized (mutex) {
return priorityQueue.isEmpty();
}
}
@Override
public SpiderFutureTask<?> get() {
synchronized (mutex) {
return priorityQueue.poll();
}
}
}

@ -0,0 +1,33 @@
package org.spiderflow.concurrent;
import org.apache.commons.lang3.RandomUtils;
import org.spiderflow.model.SpiderNode;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class RandomThreadSubmitStrategy implements ThreadSubmitStrategy{
private List<SpiderFutureTask<?>> taskList = new CopyOnWriteArrayList<>();
@Override
public Comparator<SpiderNode> comparator() {
return (o1, o2) -> RandomUtils.nextInt(0,3) - 1;
}
@Override
public void add(SpiderFutureTask<?> task) {
taskList.add(task);
}
@Override
public boolean isEmpty() {
return taskList.isEmpty();
}
@Override
public SpiderFutureTask<?> get() {
return taskList.remove(RandomUtils.nextInt(0, taskList.size()));
}
}

@ -0,0 +1,201 @@
package org.spiderflow.concurrent;
import org.spiderflow.model.SpiderNode;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class SpiderFlowThreadPoolExecutor {
/**
* 线
*/
private int maxThreads;
/**
* 线
*/
private ThreadPoolExecutor executor;
/**
* 线number
*/
private final AtomicInteger poolNumber = new AtomicInteger(1);
/**
* ThreadGroup
*/
private static final ThreadGroup SPIDER_FLOW_THREAD_GROUP = new ThreadGroup("spider-flow-group");
/**
* 线
*/
private static final String THREAD_POOL_NAME_PREFIX = "spider-flow-";
public SpiderFlowThreadPoolExecutor(int maxThreads) {
super();
this.maxThreads = maxThreads;
//创建线程池实例
this.executor = new ThreadPoolExecutor(maxThreads, maxThreads, 10, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), runnable -> {
//重写线程名称
return new Thread(SPIDER_FLOW_THREAD_GROUP, runnable, THREAD_POOL_NAME_PREFIX + poolNumber.getAndIncrement());
});
}
public Future<?> submit(Runnable runnable){
return this.executor.submit(runnable);
}
/**
* 线
* @param threads 线
* @return
*/
public SubThreadPoolExecutor createSubThreadPoolExecutor(int threads,ThreadSubmitStrategy submitStrategy){
return new SubThreadPoolExecutor(Math.min(maxThreads, threads),submitStrategy);
}
/**
* 线
*/
public class SubThreadPoolExecutor{
/**
* 线
*/
private int threads;
/**
*
*/
private Future<?>[] futures;
/**
*
*/
private AtomicInteger executing = new AtomicInteger(0);
/**
*
*/
private volatile boolean running = true;
/**
*
*/
private volatile boolean submitting = false;
private ThreadSubmitStrategy submitStrategy;
public SubThreadPoolExecutor(int threads,ThreadSubmitStrategy submitStrategy) {
super();
this.threads = threads;
this.futures = new Future[threads];
this.submitStrategy = submitStrategy;
}
/**
* 线
*/
public void awaitTermination(){
while(executing.get() > 0){
removeDoneFuture();
}
running = false;
//当停止时,唤醒提交任务线程使其结束
synchronized (submitStrategy){
submitStrategy.notifyAll();
}
}
private int index(){
for (int i = 0; i < threads; i++) {
if(futures[i] == null || futures[i].isDone()){
return i;
}
}
return -1;
}
/**
*
*/
private void removeDoneFuture(){
for (int i = 0; i < threads; i++) {
try {
if(futures[i] != null && futures[i].get(10,TimeUnit.MILLISECONDS) == null){
futures[i] = null;
}
} catch (Throwable t) {
//忽略异常
}
}
}
/**
* 线
*/
private void await(){
while(index() == -1){
removeDoneFuture();
}
}
/**
*
*/
public <T> Future<T> submitAsync(Runnable runnable, T value, SpiderNode node){
SpiderFutureTask<T> future = new SpiderFutureTask<>(()-> {
try {
//执行任务
runnable.run();
} finally {
//正在执行的线程数-1
executing.decrementAndGet();
}
}, value,node,this);
submitStrategy.add(future);
//如果是第一次调用submitSync方法则启动提交任务线程
if(!submitting){
submitting = true;
CompletableFuture.runAsync(this::submit);
}
synchronized (submitStrategy){
//通知继续从集合中取任务提交到线程池中
submitStrategy.notifyAll();
}
return future;
}
private void submit(){
while(running){
try {
synchronized (submitStrategy){
//如果集合是空的,则等待提交
if(submitStrategy.isEmpty()){
submitStrategy.wait(); //等待唤醒
}
}
//当该线程被唤醒时,把集合中所有任务都提交到线程池中
while(!submitStrategy.isEmpty()){
//从提交策略中获取任务提交到线程池中
SpiderFutureTask<?> futureTask = submitStrategy.get();
//如果没有空闲线程且在线程池中提交,则直接运行
if(index() == -1 && Thread.currentThread().getThreadGroup() == SPIDER_FLOW_THREAD_GROUP){
futureTask.run();
}else{
//等待有空闲线程时在提交
await();
//提交任务至线程池中
futures[index()] = executor.submit(futureTask);
}
}
} catch (InterruptedException ignored) {
}
}
}
}
}

@ -0,0 +1,26 @@
package org.spiderflow.concurrent;
import java.util.concurrent.FutureTask;
import org.spiderflow.concurrent.SpiderFlowThreadPoolExecutor.SubThreadPoolExecutor;
import org.spiderflow.model.SpiderNode;
public class SpiderFutureTask<V> extends FutureTask {
private SubThreadPoolExecutor executor;
private SpiderNode node;
public SpiderFutureTask(Runnable runnable, V result, SpiderNode node,SubThreadPoolExecutor executor) {
super(runnable,result);
this.executor = executor;
this.node = node;
}
public SubThreadPoolExecutor getExecutor() {
return executor;
}
public SpiderNode getNode() {
return node;
}
}

@ -0,0 +1,16 @@
package org.spiderflow.concurrent;
import org.spiderflow.model.SpiderNode;
import java.util.Comparator;
public interface ThreadSubmitStrategy {
Comparator<SpiderNode> comparator();
void add(SpiderFutureTask<?> task);
boolean isEmpty();
SpiderFutureTask<?> get();
}

@ -0,0 +1,9 @@
package org.spiderflow.context;
import java.util.HashMap;
/**
* Cookie
*/
public class CookieContext extends HashMap<String, String> {
}

@ -0,0 +1,120 @@
package org.spiderflow.context;
import org.spiderflow.concurrent.SpiderFlowThreadPoolExecutor.SubThreadPoolExecutor;
import org.spiderflow.model.SpiderNode;
import org.spiderflow.model.SpiderOutput;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
/**
*
* @author jmxd
*
*/
public class SpiderContext extends HashMap<String, Object>{
private String id = UUID.randomUUID().toString().replace("-", "");
/**
* ID
*/
private String flowId;
private static final long serialVersionUID = 8379177178417619790L;
/**
* 线
*/
private SubThreadPoolExecutor threadPool;
/**
*
*/
private SpiderNode rootNode;
/**
*
*/
private volatile boolean running = true;
/**
* Future
*/
private LinkedBlockingQueue<Future<?>> futureQueue = new LinkedBlockingQueue<>();
/**
* Cookie
*/
private CookieContext cookieContext = new CookieContext();
public List<SpiderOutput> getOutputs() {
return Collections.emptyList();
}
public <T> T get(String key){
return (T) super.get(key);
}
public <T> T get(String key,T defaultValue){
T value = this.get(key);
return value == null ? defaultValue : value;
}
public String getFlowId() {
return flowId;
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
public LinkedBlockingQueue<Future<?>> getFutureQueue() {
return futureQueue;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
public void addOutput(SpiderOutput output){
}
public SubThreadPoolExecutor getThreadPool() {
return threadPool;
}
public void setThreadPool(SubThreadPoolExecutor threadPool) {
this.threadPool = threadPool;
}
public SpiderNode getRootNode() {
return rootNode;
}
public void setRootNode(SpiderNode rootNode) {
this.rootNode = rootNode;
}
public String getId() {
return id;
}
public CookieContext getCookieContext() {
return cookieContext;
}
public void pause(String nodeId,String event,String key,Object value){}
public void resume(){}
public void stop(){}
}

@ -0,0 +1,21 @@
package org.spiderflow.context;
import com.alibaba.ttl.TransmittableThreadLocal;
public class SpiderContextHolder {
private static final ThreadLocal<SpiderContext> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static SpiderContext get() {
return THREAD_LOCAL.get();
}
public static void set(SpiderContext context) {
THREAD_LOCAL.set(context);
}
public static void remove() {
THREAD_LOCAL.remove();
}
}

@ -0,0 +1,23 @@
package org.spiderflow.enums;
/**
*
*
* @author BillDowney
* @date 202044 1:32:53
*/
public enum FlowNoticeType {
/**
*
*/
startNotice,
/**
*
*/
exceptionNotice,
/**
*
*/
endNotice
}

@ -0,0 +1,35 @@
package org.spiderflow.enums;
import java.util.LinkedHashMap;
import java.util.Map;
/**
*
*
* @author BillDowney
* @date 202043 3:26:18
*/
public enum FlowNoticeWay {
email("邮件通知");
private FlowNoticeWay(String title) {
this.title = title;
}
private String title;
@Override
public String toString() {
return this.name() + ":" + this.title;
}
public static Map<String, String> getMap() {
Map<String, String> map = new LinkedHashMap<String, String>();
for (FlowNoticeWay type : FlowNoticeWay.values()) {
map.put(type.name(), type.toString());
}
return map;
}
}

@ -0,0 +1,7 @@
package org.spiderflow.executor;
public interface FunctionExecutor {
String getFunctionPrefix();
}

@ -0,0 +1,6 @@
package org.spiderflow.executor;
public interface FunctionExtension {
Class<?> support();
}

@ -0,0 +1,9 @@
package org.spiderflow.executor;
import org.spiderflow.model.Plugin;
public interface PluginConfig {
Plugin plugin();
}

@ -0,0 +1,46 @@
package org.spiderflow.executor;
import java.util.Map;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.model.Shape;
import org.spiderflow.model.SpiderNode;
/**
*
* @author jmxd
*
*/
public interface ShapeExecutor {
String LOOP_VARIABLE_NAME = "loopVariableName";
String LOOP_COUNT = "loopCount";
String THREAD_COUNT = "threadCount";
default Shape shape(){
return null;
}
/**
*
* @return
*/
String supportShape();
/**
*
* @param node
* @param context
* @param variables
*/
void execute(SpiderNode node, SpiderContext context, Map<String, Object> variables);
default boolean allowExecuteNext(SpiderNode node, SpiderContext context, Map<String, Object> variables){
return true;
}
default boolean isThread(){
return true;
}
}

@ -0,0 +1,9 @@
package org.spiderflow.expression;
import java.util.List;
public interface DynamicMethod {
Object execute(String methodName, List<Object> parameters);
}

@ -0,0 +1,49 @@
package org.spiderflow.io;
public class Line {
private long from;
private String text;
private long to;
public Line(long from, String text, long to) {
this.from = from;
this.text = text;
this.to = to;
}
public long getFrom() {
return from;
}
public void setFrom(long from) {
this.from = from;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public long getTo() {
return to;
}
public void setTo(long to) {
this.to = to;
}
@Override
public String toString() {
return "Line{" +
"from=" + from +
", text='" + text + '\'' +
", to=" + to +
'}';
}
}

@ -0,0 +1,146 @@
package org.spiderflow.io;
import java.io.Closeable;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class RandomAccessFileReader implements Closeable {
private RandomAccessFile raf;
/**
* index
*/
private long index;
/**
*
*/
private boolean reversed;
/**
*
*/
private int bufSize;
public RandomAccessFileReader(RandomAccessFile raf, long index, boolean reversed) throws IOException {
this(raf, index, 1024, reversed);
}
public RandomAccessFileReader(RandomAccessFile raf, long index, int bufSize, boolean reversed) throws IOException {
if (raf == null) {
throw new NullPointerException("file is null");
}
this.raf = raf;
this.reversed = reversed;
this.bufSize = bufSize;
this.index = index;
this.init();
}
private void init() throws IOException {
if (reversed) {
this.index = this.index == -1 ? this.raf.length() : Math.min(this.index, this.raf.length());
} else {
this.index = Math.min(Math.max(this.index, 0), this.raf.length());
}
if (this.index > 0) {
this.raf.seek(this.index);
}
}
/**
* n
*
* @param n
* @param keywords
* @param matchcase
* @param regx
* @return Line
*/
public List<Line> readLine(int n, String keywords, boolean matchcase, boolean regx) throws IOException {
List<Line> lines = new ArrayList<>(n);
long lastCRLFIndex = reversed ? this.index : (this.index > 0 ? this.index + 1 : -1);
boolean find = keywords == null || keywords.isEmpty();
Pattern pattern = regx && !find ? Pattern.compile(keywords) : null;
while (n > 0) {
byte[] buf = reversed ? new byte[(int) Math.min(this.bufSize, this.index)] : new byte[this.bufSize];
if (this.reversed) {
if (this.index == 0) {
break;
}
this.raf.seek(this.index -= buf.length);
}
int len = this.raf.read(buf, 0, buf.length);
if (len == -1) { //已读完
break;
}
for (int i = 0; i < len && n > 0; i++) {
int readIndex = reversed ? len - i - 1 : i;
if (isCRLF(buf[readIndex])) { //如果读取到\r或\n
if (Math.abs(this.index + readIndex - lastCRLFIndex) > 1) { //两行之间的间距,当=1时则代表有\r\n,\n\r,\r\r,\n\n四种情况之一
long fromIndex = reversed ? this.index + readIndex : lastCRLFIndex; //计算起止位置
long endIndex = reversed ? lastCRLFIndex : this.index + readIndex; //计算终止位置
Line line = readLine(fromIndex + 1, endIndex); //取出文本
if (find || (find = (pattern == null ? find(line.getText(), keywords, matchcase) : find(line.getText(), pattern)))) { //定位查找,使被查找的行始终在第一行
if (reversed) {
lines.add(0, line); //反向查找时插入到List头部
} else {
lines.add(line);
}
n--;
}
}
lastCRLFIndex = this.index + readIndex; //记录上次读取到的\r或\n位置
}
}
if (!reversed) {
this.index += buf.length;
}
}
if (reversed && n > 0 && lastCRLFIndex > 1 && (find || lines.size() > 0)) {
lines.add(0, readLine(0, lastCRLFIndex));
}
return lines;
}
private boolean find(String text, String keywords, boolean matchcase) {
return matchcase ? text.contains(keywords) : text.toLowerCase().contains(keywords.toLowerCase());
}
private boolean find(String text, Pattern pattern) {
return pattern.matcher(text).find();
}
/**
*
*
* @param fromIndex
* @param endIndex
* @return Line
* @throws IOException
*/
private Line readLine(long fromIndex, long endIndex) throws IOException {
long index = this.raf.getFilePointer();
this.raf.seek(fromIndex);
byte[] buf = new byte[(int) (endIndex - fromIndex)];
this.raf.read(buf, 0, buf.length);
Line line = new Line(fromIndex, new String(buf), endIndex);
this.raf.seek(index);
return line;
}
private boolean isCRLF(byte b) {
return b == 13 || b == 10;
}
@Override
public void close() throws IOException {
if (this.raf != null) {
this.raf.close();
}
}
}

@ -0,0 +1,58 @@
package org.spiderflow.io;
import java.io.InputStream;
import java.util.Map;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import com.alibaba.fastjson.JSON;
public interface SpiderResponse {
@Comment("获取返回状态码")
@Example("${resp.statusCode}")
int getStatusCode();
@Comment("获取网页标题")
@Example("${resp.title}")
String getTitle();
@Comment("获取网页html")
@Example("${resp.html}")
String getHtml();
@Comment("获取json")
@Example("${resp.json}")
default Object getJson(){
return JSON.parse(getHtml());
}
@Comment("获取cookies")
@Example("${resp.cookies}")
Map<String,String> getCookies();
@Comment("获取headers")
@Example("${resp.headers}")
Map<String,String> getHeaders();
@Comment("获取byte[]")
@Example("${resp.bytes}")
byte[] getBytes();
@Comment("获取ContentType")
@Example("${resp.contentType}")
String getContentType();
@Comment("获取当前url")
@Example("${resp.url}")
String getUrl();
@Example("${resp.setCharset('UTF-8')}")
default void setCharset(String charset){
}
@Example("${resp.stream}")
default InputStream getStream(){
return null;
}
}

@ -0,0 +1,17 @@
package org.spiderflow.listener;
import org.spiderflow.context.SpiderContext;
public interface SpiderListener {
/**
*
*/
void beforeStart(SpiderContext context);
/**
*
*/
void afterEnd(SpiderContext context);
}

@ -0,0 +1,108 @@
package org.spiderflow.model;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.annotation.Return;
public class Grammer {
private String owner;
private String method;
private String comment;
private String example;
private String function;
private List<String> returns;
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getFunction() {
return function;
}
public void setFunction(String function) {
this.function = function;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getExample() {
return example;
}
public void setExample(String example) {
this.example = example;
}
public List<String> getReturns() {
return returns;
}
public void setReturns(List<String> returns) {
this.returns = returns;
}
public static List<Grammer> findGrammers(Class<?> clazz,String function,String owner,boolean mustStatic){
Method[] methods = clazz.getDeclaredMethods();
List<Grammer> grammers = new ArrayList<>();
for (Method method : methods) {
if(Modifier.isPublic(method.getModifiers()) && (Modifier.isStatic(method.getModifiers())||!mustStatic)){
Grammer grammer = new Grammer();
grammer.setMethod(method.getName());
Comment comment = method.getAnnotation(Comment.class);
if(comment != null){
grammer.setComment(comment.value());
}
Example example = method.getAnnotation(Example.class);
if(example != null){
grammer.setExample(example.value());
}
Return returns = method.getAnnotation(Return.class);
if(returns != null){
Class<?>[] clazzs = returns.value();
List<String> returnTypes = new ArrayList<>();
for (int i = 0; i < clazzs.length; i++) {
returnTypes.add(clazzs[i].getSimpleName());
}
grammer.setReturns(returnTypes);
}else{
grammer.setReturns(Collections.singletonList(method.getReturnType().getSimpleName()));
}
grammer.setFunction(function);
grammer.setOwner(owner);
grammers.add(grammer);
}
}
return grammers;
}
}

@ -0,0 +1,49 @@
package org.spiderflow.model;
public class JsonBean<T> {
private Integer code = 1;
private String message = "执行成功";
private T data;
public JsonBean(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public JsonBean(Integer code, String message) {
this.code = code;
this.message = message;
}
public JsonBean(T data) {
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

@ -0,0 +1,30 @@
package org.spiderflow.model;
public class Plugin {
private String name;
private String url;
public Plugin(String name, String url) {
this.name = name;
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}

@ -0,0 +1,54 @@
package org.spiderflow.model;
public class Shape {
private String name;
private String label;
private String title;
private String image;
private String desc;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}

@ -0,0 +1,55 @@
package org.spiderflow.model;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.exception.ExceptionUtils;
public class SpiderLog {
private String level;
private String message;
private List<Object> variables;
public SpiderLog(String level,String message, List<Object> variables) {
if(variables != null && variables.size() > 0){
List<Object> nVariables = new ArrayList<>(variables.size());
for (Object object : variables) {
if(object instanceof Throwable){
nVariables.add(ExceptionUtils.getStackTrace((Throwable) object));
}else{
nVariables.add(object);
}
}
this.variables = nVariables;
}
this.level = level;
this.message = message;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<Object> getVariables() {
return variables;
}
public void setVariables(List<Object> variables) {
this.variables = variables;
}
}

@ -0,0 +1,205 @@
package org.spiderflow.model;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import com.alibaba.fastjson.JSONArray;
/**
*
* @author jmxd
*
*/
public class SpiderNode {
/**
* Json
*/
private Map<String,Object> jsonProperty = new HashMap<>();
/**
*
*/
private List<SpiderNode> nextNodes = new ArrayList<>();
/**
*
*/
private List<SpiderNode> prevNodes = new ArrayList<>();
/**
* ID
*/
private Set<String> parentNodes;
/**
*
*/
private Map<String,String> condition = new HashMap<>();
/**
*
*/
private Map<String,String> exception = new HashMap<>();
/**
*
*/
private Map<String,String> transmitVariable = new HashMap<>();
/**
*
*/
private String nodeName;
/**
* ID
*/
private String nodeId;
/**
* ,
*/
private AtomicInteger counter = new AtomicInteger();
public String getNodeId() {
return nodeId;
}
public void setNodeId(String nodeId) {
this.nodeId = nodeId;
}
public String getNodeName() {
return nodeName;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public String getStringJsonValue(String key){
String value = (String) this.jsonProperty.get(key);
if(value != null){
value = StringEscapeUtils.unescapeHtml4(value);
}
return value;
}
public String getStringJsonValue(String key,String defaultValue){
String value = getStringJsonValue(key);
return StringUtils.isNotBlank(value) ? value : defaultValue;
}
public List<Map<String,String>> getListJsonValue(String ... keys){
List<JSONArray> arrays = new ArrayList<>();
int size = -1;
List<Map<String,String>> result = new ArrayList<>();
for (int i = 0; i < keys.length; i++) {
JSONArray jsonArray = (JSONArray) this.jsonProperty.get(keys[i]);
if(jsonArray != null){
if(size == -1){
size = jsonArray.size();
}else if(size != jsonArray.size()){
throw new ArrayIndexOutOfBoundsException();
}
arrays.add(jsonArray);
}
}
for (int i = 0;i < size;i++) {
Map<String,String> item = new HashMap<>();
for (int j = 0; j < keys.length; j++) {
String val = arrays.get(j).getString(i);
if(val != null){
val = StringEscapeUtils.unescapeHtml4(val);
}
item.put(keys[j],val);
}
result.add(item);
}
return result;
}
public void setJsonProperty(Map<String, Object> jsonProperty) {
this.jsonProperty = jsonProperty;
}
public void addNextNode(SpiderNode nextNode){
nextNode.prevNodes.add(this);
this.nextNodes.add(nextNode);
}
public String getExceptionFlow(String fromNodeId) {
return exception.get(fromNodeId);
}
public boolean isTransmitVariable(String fromNodeId) {
String value = transmitVariable.get(fromNodeId);
return value == null || "1".equalsIgnoreCase(value);
}
public void setTransmitVariable(String fromNodeId,String value){
this.transmitVariable.put(fromNodeId,value);
}
public void setExceptionFlow(String fromNodeId,String value){
this.exception.put(fromNodeId,value);
}
public List<SpiderNode> getNextNodes() {
return nextNodes;
}
public String getCondition(String fromNodeId) {
return condition.get(fromNodeId);
}
public void setCondition(String fromNodeId,String condition) {
this.condition.put(fromNodeId, condition);
}
public void increment(){
counter.incrementAndGet();
}
public void decrement(){
counter.decrementAndGet();
}
public boolean hasLeftNode(String nodeId){
if(parentNodes == null){
Set<String> parents = new HashSet<>();
generateParents(parents);
this.parentNodes = parents;
}
return this.parentNodes.contains(nodeId);
}
private void generateParents(Set<String> parents){
for (SpiderNode prevNode : prevNodes) {
if(parents.add(prevNode.nodeId)){
prevNode.generateParents(parents);
}
}
}
public boolean isDone(){
return isDone(new HashSet<>());
}
public boolean isDone(Set<String> visited){
if(this.counter.get() == 0){
for (SpiderNode prevNode : prevNodes) {
if(visited.add(nodeId)&&!prevNode.isDone(visited)){
return false;
}
}
return true;
}
return false;
}
@Override
public String toString() {
return "SpiderNode [jsonProperty=" + jsonProperty + ", nextNodes=" + nextNodes + ", condition=" + condition
+ ", nodeName=" + nodeName + ", nodeId=" + nodeId + "]";
}
}

@ -0,0 +1,70 @@
package org.spiderflow.model;
import java.util.ArrayList;
import java.util.List;
public class SpiderOutput {
/**
*
*/
private String nodeName;
/**
* Id
*/
private String nodeId;
/**
*
*/
private List<String> outputNames = new ArrayList<>();
/**
*
*/
private List<Object> values = new ArrayList<>();
public String getNodeName() {
return nodeName;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public List<String> getOutputNames() {
return outputNames;
}
public void setOutputNames(List<String> outputNames) {
this.outputNames = outputNames;
}
public List<Object> getValues() {
return values;
}
public void setValues(List<Object> values) {
this.values = values;
}
public void addOutput(String name,Object value){
this.outputNames.add(name);
this.values.add(value);
}
public String getNodeId() {
return nodeId;
}
public void setNodeId(String nodeId) {
this.nodeId = nodeId;
}
@Override
public String toString() {
return "SpiderOutput [nodeName=" + nodeName + ", nodeId=" + nodeId + ", outputNames=" + outputNames
+ ", values=" + values + "]";
}
}

@ -0,0 +1,31 @@
package org.spiderflow.utils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Maps {
public static <K,V> Map<K,V> add(Map<K,V> srcMap,K k,V v){
HashMap<K, V> destMap = new HashMap<>(srcMap);
destMap.put(k, v);
return destMap;
}
public static <K,V> Map<K,V> newMap(K key,V value){
HashMap<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
public static <K,V> Map<K,V> add(Map<K,V> srcMap,List<K> ks,List<V> vs){
HashMap<K, V> destMap = new HashMap<>(srcMap);
if(ks != null && vs != null && ks.size() == vs.size()){
int size = ks.size();
for (int i = 0; i < size; i++) {
destMap.put(ks.get(0), vs.get(0));
}
}
return destMap;
}
}

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow</artifactId>
<version>0.5.0</version>
</parent>
<artifactId>spider-flow-core</artifactId>
<name>spider-flow-core</name>
<url>https://gitee.com/jmxd/spider-flow/tree/master/spider-flow-core</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.spiderflow</groupId>
<artifactId>spider-flow-api</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,358 @@
package org.spiderflow.core;
import com.alibaba.ttl.TtlRunnable;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.concurrent.*;
import org.spiderflow.concurrent.SpiderFlowThreadPoolExecutor.SubThreadPoolExecutor;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.context.SpiderContextHolder;
import org.spiderflow.core.executor.shape.LoopExecutor;
import org.spiderflow.core.model.SpiderFlow;
import org.spiderflow.core.service.FlowNoticeService;
import org.spiderflow.core.utils.ExecutorsUtils;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.core.utils.SpiderFlowUtils;
import org.spiderflow.enums.FlowNoticeType;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.listener.SpiderListener;
import org.spiderflow.model.SpiderNode;
import org.spiderflow.model.SpiderOutput;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
*
* @author jmxd
*/
@Component
public class Spider {
@Autowired(required = false)
private List<SpiderListener> listeners;
@Value("${spider.thread.max:64}")
private Integer totalThreads;
@Value("${spider.thread.default:8}")
private Integer defaultThreads;
@Value("${spider.detect.dead-cycle:5000}")
private Integer deadCycle;
@Autowired
private FlowNoticeService flowNoticeService;
public static SpiderFlowThreadPoolExecutor executorInstance;
private static final String ATOMIC_DEAD_CYCLE = "__atomic_dead_cycle";
private static Logger logger = LoggerFactory.getLogger(Spider.class);
@PostConstruct
private void init() {
executorInstance = new SpiderFlowThreadPoolExecutor(totalThreads);
}
public List<SpiderOutput> run(SpiderFlow spiderFlow, SpiderContext context, Map<String, Object> variables) {
if (variables == null) {
variables = new HashMap<>();
}
SpiderNode root = SpiderFlowUtils.loadXMLFromString(spiderFlow.getXml());
// 流程开始通知
flowNoticeService.sendFlowNotice(spiderFlow, FlowNoticeType.startNotice);
executeRoot(root, context, variables);
// 流程结束通知
flowNoticeService.sendFlowNotice(spiderFlow, FlowNoticeType.endNotice);
return context.getOutputs();
}
public List<SpiderOutput> run(SpiderFlow spiderFlow, SpiderContext context) {
return run(spiderFlow, context, new HashMap<>());
}
public void runWithTest(SpiderNode root, SpiderContext context) {
//将上下文存到ThreadLocal里以便后续使用
SpiderContextHolder.set(context);
//死循环检测的计数器(死循环检测只在测试时有效)
AtomicInteger executeCount = new AtomicInteger(0);
//存入到上下文中,以供后续检测
context.put(ATOMIC_DEAD_CYCLE, executeCount);
//执行根节点
executeRoot(root, context, new HashMap<>());
//当爬虫任务执行完毕时,判断是否超过预期
if (executeCount.get() > deadCycle) {
logger.error("检测到可能出现死循环,测试终止");
} else {
logger.info("测试完毕!");
}
//将上下文从ThreadLocal移除防止内存泄漏
SpiderContextHolder.remove();
}
/**
*
*/
private void executeRoot(SpiderNode root, SpiderContext context, Map<String, Object> variables) {
//获取当前流程执行线程数
int nThreads = NumberUtils.toInt(root.getStringJsonValue(ShapeExecutor.THREAD_COUNT), defaultThreads);
String strategy = root.getStringJsonValue("submit-strategy");
ThreadSubmitStrategy submitStrategy;
//选择提交策略这里一定要使用new,不能与其他实例共享
if("linked".equalsIgnoreCase(strategy)){
submitStrategy = new LinkedThreadSubmitStrategy();
}else if("child".equalsIgnoreCase(strategy)){
submitStrategy = new ChildPriorThreadSubmitStrategy();
}else if("parent".equalsIgnoreCase(strategy)){
submitStrategy = new ParentPriorThreadSubmitStrategy();
}else{
submitStrategy = new RandomThreadSubmitStrategy();
}
//创建子线程池,采用一父多子的线程池,子线程数不能超过总线程数(超过时进入队列等待),+1是因为会占用一个线程用来调度执行下一级
SubThreadPoolExecutor pool = executorInstance.createSubThreadPoolExecutor(Math.max(nThreads,1) + 1,submitStrategy);
context.setRootNode(root);
context.setThreadPool(pool);
//触发监听器
if (listeners != null) {
listeners.forEach(listener -> listener.beforeStart(context));
}
Comparator<SpiderNode> comparator = submitStrategy.comparator();
//启动一个线程开始执行任务,并监听其结束并执行下一级
Future<?> f = pool.submitAsync(TtlRunnable.get(() -> {
try {
//执行具体节点
Spider.this.executeNode(null, root, context, variables);
Queue<Future<?>> queue = context.getFutureQueue();
//循环从队列中获取Future,直到队列为空结束,当任务完成时,则执行下一级
while (!queue.isEmpty()) {
try {
//TODO 这里应该是取出最先执行完毕的任务
Optional<Future<?>> max = queue.stream().filter(Future::isDone).max((o1, o2) -> {
try {
return comparator.compare(((SpiderTask) o1.get()).node, ((SpiderTask) o2.get()).node);
} catch (InterruptedException | ExecutionException e) {
}
return 0;
});
if (max.isPresent()) { //判断任务是否完成
queue.remove(max.get());
if (context.isRunning()) { //检测是否运行中(当在页面中点击"停止"时,此值为false,其余为true)
SpiderTask task = (SpiderTask) max.get().get();
task.node.decrement(); //任务执行完毕,计数器减一(该计数器是给Join节点使用)
if (task.executor.allowExecuteNext(task.node, context, task.variables)) { //判断是否允许执行下一级
logger.debug("执行节点[{}:{}]完毕", task.node.getNodeName(), task.node.getNodeId());
//执行下一级
Spider.this.executeNextNodes(task.node, context, task.variables);
} else {
logger.debug("执行节点[{}:{}]完毕,忽略执行下一节点", task.node.getNodeName(), task.node.getNodeId());
}
}
}
//睡眠1ms,让出cpu
Thread.sleep(1);
} catch (InterruptedException ignored) {
} catch (Throwable t){
logger.error("程序发生异常",t);
}
}
//等待线程池结束
pool.awaitTermination();
} finally {
//触发监听器
if (listeners != null) {
listeners.forEach(listener -> listener.afterEnd(context));
}
}
}), null, root);
try {
f.get(); //阻塞等待所有任务执行完毕
} catch (InterruptedException | ExecutionException ignored) {}
}
/**
*
*/
private void executeNextNodes(SpiderNode node, SpiderContext context, Map<String, Object> variables) {
List<SpiderNode> nextNodes = node.getNextNodes();
if (nextNodes != null) {
for (SpiderNode nextNode : nextNodes) {
executeNode(node, nextNode, context, variables);
}
}
}
/**
*
*/
public void executeNode(SpiderNode fromNode, SpiderNode node, SpiderContext context, Map<String, Object> variables) {
String shape = node.getStringJsonValue("shape");
if (StringUtils.isBlank(shape)) {
executeNextNodes(node, context, variables);
return;
}
//判断箭头上的条件,如果不成立则不执行
if (!executeCondition(fromNode, node, variables, context)) {
return;
}
logger.debug("执行节点[{}:{}]", node.getNodeName(), node.getNodeId());
//找到对应的执行器
ShapeExecutor executor = ExecutorsUtils.get(shape);
if (executor == null) {
logger.error("执行失败,找不到对应的执行器:{}", shape);
context.setRunning(false);
}
int loopCount = 1; //循环次数默认为1,如果节点有循环属性且填了循环次数/集合,则取出循环次数
int loopStart = 0; //循环起始位置
int loopEnd = 1; //循环结束位置
String loopCountStr = node.getStringJsonValue(ShapeExecutor.LOOP_COUNT);
Object loopArray = null;
boolean isLoop = false;
if (isLoop = StringUtils.isNotBlank(loopCountStr)) {
try {
loopArray = ExpressionUtils.execute(loopCountStr, variables);
if(loopArray == null){
loopCount = 0;
}else if(loopArray instanceof Collection){
loopCount = ((Collection)loopArray).size();
loopArray = ((Collection)loopArray).toArray();
}else if(loopArray.getClass().isArray()){
loopCount = Array.getLength(loopArray);
}else{
loopCount = NumberUtils.toInt(loopArray.toString(),0);
loopArray = null;
}
loopEnd = loopCount;
if(loopCount > 0){
loopStart = Math.max(NumberUtils.toInt(node.getStringJsonValue(LoopExecutor.LOOP_START), 0),0);
int end = NumberUtils.toInt(node.getStringJsonValue(LoopExecutor.LOOP_END), -1);
if(end >=0){
loopEnd = Math.min(end,loopEnd);
}else{
loopEnd = Math.max(loopEnd + end + 1,0);
}
}
logger.info("获取循环次数{}={}", loopCountStr, loopCount);
} catch (Throwable t) {
loopCount = 0;
logger.error("获取循环次数失败,异常信息:{}", t);
}
}
if (loopCount > 0) {
//获取循环下标的变量名称
String loopVariableName = node.getStringJsonValue(ShapeExecutor.LOOP_VARIABLE_NAME);
String loopItem = node.getStringJsonValue(LoopExecutor.LOOP_ITEM,"item");
List<SpiderTask> tasks = new ArrayList<>();
for (int i = loopStart; i < loopEnd; i++) {
node.increment(); //节点执行次数+1(后续Join节点使用)
if (context.isRunning()) {
Map<String, Object> nVariables = new HashMap<>();
// 判断是否需要传递变量
if(fromNode == null || node.isTransmitVariable(fromNode.getNodeId())){
nVariables.putAll(variables);
}
if(isLoop){
// 存入下标变量
if (!StringUtils.isBlank(loopVariableName)) {
nVariables.put(loopVariableName, i);
}
// 存入item
nVariables.put(loopItem,loopArray == null ? i : Array.get(loopArray, i));
}
tasks.add(new SpiderTask(TtlRunnable.get(() -> {
if (context.isRunning()) {
try {
//死循环检测,当执行节点次数大于阈值时,结束本次测试
AtomicInteger executeCount = context.get(ATOMIC_DEAD_CYCLE);
if (executeCount != null && executeCount.incrementAndGet() > deadCycle) {
context.setRunning(false);
return;
}
//执行节点具体逻辑
executor.execute(node, context, nVariables);
//当未发生异常时移除ex变量
nVariables.remove("ex");
} catch (Throwable t) {
nVariables.put("ex", t);
logger.error("执行节点[{}:{}]出错,异常信息:{}", node.getNodeName(), node.getNodeId(), t);
}
}
}), node, nVariables, executor));
}
}
LinkedBlockingQueue<Future<?>> futureQueue = context.getFutureQueue();
for (SpiderTask task : tasks) {
if(executor.isThread()){ //判断节点是否是异步运行
//提交任务至线程池中,并将Future添加到队列末尾
futureQueue.add(context.getThreadPool().submitAsync(task.runnable, task, node));
}else{
FutureTask<SpiderTask> futureTask = new FutureTask<>(task.runnable, task);
futureTask.run();
futureQueue.add(futureTask);
}
}
}
}
/**
*
*/
private boolean executeCondition(SpiderNode fromNode, SpiderNode node, Map<String, Object> variables, SpiderContext context) {
if (fromNode != null) {
boolean hasException = variables.get("ex") != null;
String exceptionFlow = node.getExceptionFlow(fromNode.getNodeId());
//当出现异常流转 : 1
//未出现异常流转 : 2
if(("1".equalsIgnoreCase(exceptionFlow) && !hasException) || ("2".equalsIgnoreCase(exceptionFlow) && hasException)){
return false;
}
String condition = node.getCondition(fromNode.getNodeId());
if (StringUtils.isNotBlank(condition)) { // 判断是否有条件
Object result = null;
try {
result = ExpressionUtils.execute(condition, variables);
} catch (Exception e) {
logger.error("判断{}出错,异常信息:{}", condition, e);
}
if (result != null) {
boolean isContinue = "true".equals(result) || Objects.equals(result, true);
logger.debug("判断{}={}", condition, isContinue);
return isContinue;
}
return false;
}
}
return true;
}
class SpiderTask{
Runnable runnable;
SpiderNode node;
Map<String,Object> variables;
ShapeExecutor executor;
public SpiderTask(Runnable runnable, SpiderNode node, Map<String, Object> variables,ShapeExecutor executor) {
this.runnable = runnable;
this.node = node;
this.variables = variables;
this.executor = executor;
}
}
}

@ -0,0 +1,88 @@
package org.spiderflow.core.executor.function;
import org.apache.commons.codec.binary.Base64;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* Base64 NPE
* @author Administrator
*
*/
@Component
@Comment("base64常用方法")
public class Base64FunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "base64";
}
@Comment("根据byte[]进行base64加密")
@Example("${base64.encode(resp.bytes)}")
public static String encode(byte[] bytes){
return bytes != null ? Base64.encodeBase64String(bytes) : null;
}
@Comment("根据String进行base64加密")
@Example("${base64.encode(resp.bytes,'UTF-8')}")
public static String encode(String content,String charset){
return encode(StringFunctionExecutor.bytes(content,charset));
}
@Comment("根据String进行base64加密")
@Example("${base64.encode(resp.html)}")
public static String encode(String content){
return encode(StringFunctionExecutor.bytes(content));
}
@Comment("根据byte[]进行base64加密")
@Example("${base64.encodeBytes(resp.bytes)}")
public static byte[] encodeBytes(byte[] bytes){
return bytes != null ? Base64.encodeBase64(bytes) : null;
}
@Comment("根据String进行base64加密")
@Example("${base64.encodeBytes(resp.html,'UTF-8')}")
public static byte[] encodeBytes(String content,String charset){
return encodeBytes(StringFunctionExecutor.bytes(content,charset));
}
@Comment("根据String进行base64加密")
@Example("${base64.encodeBytes(resp.html)}")
public static byte[] encodeBytes(String content){
return encodeBytes(StringFunctionExecutor.bytes(content));
}
@Comment("根据String进行base64解密")
@Example("${base64.decode(resp.html)}")
public static byte[] decode(String base64){
return base64 != null ? Base64.decodeBase64(base64) :null;
}
@Comment("根据byte[]进行base64解密")
@Example("${base64.decode(resp.bytes)}")
public static byte[] decode(byte[] base64){
return base64 != null ? Base64.decodeBase64(base64) :null;
}
@Comment("根据String进行base64解密")
@Example("${base64.decodeString(resp.html)}")
public static String decodeString(String base64){
return base64 != null ? new String(Base64.decodeBase64(base64)) :null;
}
@Comment("根据byte[]进行base64解密")
@Example("${base64.decodeString(resp.bytes)}")
public static String decodeString(byte[] base64){
return base64 != null ? new String(Base64.decodeBase64(base64)) :null;
}
@Comment("根据byte[]进行base64解密")
@Example("${base64.decodeString(resp.bytes,'UTF-8')}")
public static String decodeString(byte[] base64,String charset){
return base64 != null ? StringFunctionExecutor.newString(Base64.decodeBase64(base64),charset) :null;
}
}

@ -0,0 +1,112 @@
package org.spiderflow.core.executor.function;
import java.text.ParseException;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* / NPE (yyyy-MM-dd HH:mm:ss)
* @author Administrator
*
*/
@Component
@Comment("日期常用方法")
public class DateFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "date";
}
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
@Comment("格式化日期")
@Example("${date.format(date.now())}")
public static String format(Date date) {
return format(date, DEFAULT_PATTERN);
}
@Comment("格式化日期")
@Example("${date.format(1569059534000l)}")
public static String format(Long millis) {
return format(millis, DEFAULT_PATTERN);
}
@Comment("格式化日期")
@Example("${date.format(date.now(),'yyyy-MM-dd')}")
public static String format(Date date, String pattern) {
return date != null ? DateFormatUtils.format(date, pattern) : null;
}
@Comment("格式化日期")
@Example("${date.format(1569059534000l,'yyyy-MM-dd')}")
public static String format(Long millis, String pattern) {
return millis != null ? DateFormatUtils.format(millis, pattern) : null;
}
@Comment("字符串转为日期类型")
@Example("${date.parse('2019-01-01 00:00:00')}")
public static Date parse(String date) throws ParseException{
return date != null ? DateUtils.parseDate(date, DEFAULT_PATTERN) : null;
}
@Comment("字符串转为日期类型")
@Example("${date.parse('2019-01-01','yyyy-MM-dd')}")
public static Date parse(String date,String pattern) throws ParseException{
return date != null ? DateUtils.parseDate(date, pattern) : null;
}
@Comment("数字为日期类型")
@Example("${date.parse(1569059534000l)}")
public static Date parse(Long millis){
return new Date(millis);
}
@Comment("获取当前时间")
@Example("${date.now()}")
public static Date now(){
return new Date();
}
@Comment("获取指定日期n年后的日期")
@Example("${date.addYears(date.now(),2)}")
public static Date addYears(Date date,int amount){
return DateUtils.addYears(date, amount);
}
@Comment("获取指定日期n月后的日期")
@Example("${date.addMonths(date.now(),2)}")
public static Date addMonths(Date date,int amount){
return DateUtils.addMonths(date, amount);
}
@Comment("获取指定日期n天后的日期")
@Example("${date.addDays(date.now(),2)}")
public static Date addDays(Date date,int amount){
return DateUtils.addDays(date, amount);
}
@Comment("获取指定日期n小时后的日期")
@Example("${date.addHours(date.now(),2)}")
public static Date addHours(Date date,int amount){
return DateUtils.addHours(date, amount);
}
@Comment("获取指定日期n分钟后的日期")
@Example("${date.addMinutes(date.now(),2)}")
public static Date addMinutes(Date date,int amount){
return DateUtils.addMinutes(date, amount);
}
@Comment("获取指定日期n秒后的日期")
@Example("${date.addSeconds(date.now(),2)}")
public static Date addSeconds(Date date,int amount){
return DateUtils.addSeconds(date, amount);
}
}

@ -0,0 +1,150 @@
package org.spiderflow.core.executor.function;
import java.util.List;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
@Component
@Comment("数据抽取常用方法")
public class ExtractFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "extract";
}
@Comment("根据jsonpath提取内容")
@Example("${extract.jsonpath(resp.json,'$.code')}")
public static Object jsonpath(Object root,String jsonpath){
return ExtractUtils.getValueByJsonPath(root, jsonpath);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regx(resp.html,'<title>(.*?)</title>')}")
public static String regx(String content,String pattern){
return ExtractUtils.getFirstMatcher(content, pattern, true);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regx(resp.html,'<title>(.*?)</title>',1)}")
public static String regx(String content,String pattern,int groupIndex){
return ExtractUtils.getFirstMatcher(content, pattern, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regx(resp.html,'<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<String> regx(String content,String pattern,List<Integer> groups){
return ExtractUtils.getFirstMatcher(content, pattern, groups);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regxs(resp.html,'<h2>(.*?)</h2>')}")
public static List<String> regxs(String content,String pattern){
return ExtractUtils.getMatchers(content, pattern, true);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regxs(resp.html,'<h2>(.*?)</h2>',1)}")
public static List<String> regxs(String content,String pattern,int groupIndex){
return ExtractUtils.getMatchers(content, pattern, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${extract.regxs(resp.html,'<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<List<String>> regxs(String content,String pattern,List<Integer> groups){
return ExtractUtils.getMatchers(content, pattern, groups);
}
@Comment("根据xpath提取内容")
@Example("${extract.xpath(resp.element(),'//title/text()')}")
public static String xpath(Element element,String xpath){
return ExtractUtils.getValueByXPath(element, xpath);
}
@Comment("根据xpath提取内容")
@Example("${extract.xpath(resp.html,'//title/text()')}")
public static String xpath(String content,String xpath){
return xpath(Jsoup.parse(content),xpath);
}
@Comment("根据xpaths提取内容")
@Example("${extract.xpaths(resp.element(),'//h2/text()')}")
public static List<String> xpaths(Element element,String xpath){
return ExtractUtils.getValuesByXPath(element, xpath);
}
@Comment("根据xpaths提取内容")
@Example("${extract.xpaths(resp.html,'//h2/text()')}")
public static List<String> xpaths(String content,String xpath){
return xpaths(Jsoup.parse(content),xpath);
}
@Comment("根据css选择器提取内容")
@Example("${extract.selectors(resp.html,'div > a')}")
public static List<String> selectors(Object object,String selector){
return ExtractUtils.getHTMLBySelector(getElement(object), selector);
}
@Comment("根据css选择器提取内容")
@Example("${extract.selector(resp.html,'div > a','text')}")
public static Object selector(Object object,String selector,String type){
if("element".equals(type)){
return ExtractUtils.getFirstElement(getElement(object), selector);
}else if("text".equals(type)){
return ExtractUtils.getFirstTextBySelector(getElement(object), selector);
}else if("outerhtml".equals(type)){
return ExtractUtils.getFirstOuterHTMLBySelector(getElement(object), selector);
}
return null;
}
@Comment("根据css选择器提取内容")
@Example("${extract.selector(resp.html,'div > a','attr','href')}")
public static String selector(Object object,String selector,String type,String attrValue){
if("attr".equals(type)){
return ExtractUtils.getFirstAttrBySelector(getElement(object), selector,attrValue);
}
return null;
}
@Comment("根据css选择器提取内容")
@Example("${extract.selector(resp.html,'div > a')}")
public static String selector(Object object,String selector){
return ExtractUtils.getFirstHTMLBySelector(getElement(object), selector);
}
@Comment("根据css选择器提取内容")
@Example("${extract.selectors(resp.html,'div > a','element')}")
public static Object selectors(Object object,String selector,String type){
if("element".equals(type)){
return ExtractUtils.getElements(getElement(object), selector);
}else if("text".equals(type)){
return ExtractUtils.getTextBySelector(getElement(object), selector);
}else if("outerhtml".equals(type)){
return ExtractUtils.getOuterHTMLBySelector(getElement(object), selector);
}
return null;
}
@Comment("根据css选择器提取内容")
@Example("${extract.selectors(resp.html,'div > a','attr','href')}")
public static Object selectors(Object object,String selector,String type,String attrValue){
if("attr".equals(type)){
return ExtractUtils.getAttrBySelector(getElement(object), selector,attrValue);
}
return null;
}
private static Element getElement(Object object){
if(object != null){
return object instanceof Element ? (Element)object:Jsoup.parse((String) object);
}
return null;
}
}

@ -0,0 +1,131 @@
package org.spiderflow.core.executor.function;
import java.io.*;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.core.utils.FileUtils;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* NPE
* @author Administrator
*
*/
@Component
@Comment("file常用方法")
public class FileFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "file";
}
/**
*
* @param path /
* @param createDirectory
* @return File
*/
private static File getFile(String path,boolean createDirectory){
File f = new File(path);
if(createDirectory&&!f.getParentFile().exists()){
f.getParentFile().mkdirs();
}
return f;
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.html,false)}")
public static void write(String path,String content,boolean append) throws IOException{
write(path,content,Charset.defaultCharset().name(),append);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.html,'UTF-8',false)}")
public static void write(String path,String content,String charset,boolean append) throws IOException{
write(path,StringFunctionExecutor.bytes(content, charset),append);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.bytes,false)}")
public static void write(String path,byte[] bytes,boolean append) throws IOException{
write(path, new ByteArrayInputStream(bytes),append);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.stream,false)}")
public static void write(String path, InputStream stream, boolean append) throws IOException {
try(FileOutputStream fos = new FileOutputStream(getFile(path,true),append)){
IOUtils.copyLarge(stream, fos);
}
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.bytes,false)}")
public static void write(String path, InputStream stream) throws IOException {
write(path, stream,false);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.html)}")
public static void write(String path,String content) throws IOException{
write(path,content,false);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.html,'UTF-8')}")
public static void write(String path,String content,String charset) throws IOException{
write(path,content,charset,false);
}
@Comment("写出文件")
@Example("${file.write('e:/result.html',resp.bytes)}")
public static void write(String path,byte[] bytes) throws IOException{
write(path,bytes,false);
}
@Comment("下载Url资源")
@Example("${file.download('e:/downloadPath',urls)}")
public static void download(String path, List<String> urls) throws IOException{
if(!CollectionUtils.isEmpty(urls)) {
for (String url : urls) {
FileUtils.downloadFile(path, url, true);
}
}
}
@Comment("下载Url资源")
@Example("${file.download('e:/downloadPath',urls)}")
public static void download(String path, String url) throws IOException {
if (url != null) {
FileUtils.downloadFile(path, url, true);
}
}
@Comment("读取文件")
@Example("${file.bytes('e:/result.html')}")
public static byte[] bytes(String path) throws IOException{
try(FileInputStream fis = new FileInputStream(getFile(path, false))){
return IOUtils.toByteArray(fis);
}
}
@Comment("读取文件")
@Example("${file.string('e:/result.html','UTF-8')}")
public static String string(String path,String charset) throws IOException{
return StringFunctionExecutor.newString(bytes(path), charset);
}
@Comment("读取文件")
@Example("${file.string('e:/result.html')}")
public static String string(String path) throws IOException{
return StringFunctionExecutor.newString(bytes(path), Charset.defaultCharset().name());
}
}

@ -0,0 +1,35 @@
package org.spiderflow.core.executor.function;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
/**
* JsonString NPE
* @author Administrator
*
*/
@Component
@Comment("json常用方法")
public class JsonFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "json";
}
@Comment("将字符串转为json对象")
@Example("${json.parse('{code : 1}')}")
public static Object parse(String jsonString){
return jsonString != null ? JSON.parse(jsonString) : null;
}
@Comment("将对象转为json字符串")
@Example("${json.stringify(objVar)}")
public static String stringify(Object object){
return object != null ? JSON.toJSONString(object) : null;
}
}

@ -0,0 +1,75 @@
package org.spiderflow.core.executor.function;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* List NPE pythonsplit()
* @author Administrator
*
*/
@Component
@Comment("list常用方法")
public class ListFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "list";
}
@Comment("获取list的长度")
@Example("${list.length(listVar)}")
public static int length(List<?> list){
return list != null ? list.size() : 0;
}
/**
*
* @param list List
* @param len
* @return List<List<?>>
*/
@Comment("分割List")
@Example("${list.split(listVar,10)}")
public static List<List<?>> split(List<?> list,int len){
List<List<?>> result = new ArrayList<>();
if (list == null || list.size() == 0 || len < 1) {
return result;
}
int size = list.size();
int count = (size + len - 1) / len;
for (int i = 0; i < count; i++) {
List<?> subList = list.subList(i * len, ((i + 1) * len > size ? size : len * (i + 1)));
result.add(subList);
}
return result;
}
@Comment("截取List")
@Example("${list.sublist(listVar,fromIndex,toIndex)}")
public static List<?> sublist(List<?> list,int fromIndex,int toIndex){
return list!= null ? list.subList(fromIndex, toIndex) : new ArrayList<>();
}
@Comment("过滤字符串list元素")
@Example("${listVar.filterStr(pattern)}")
public static List<String> filterStr(List<String> list, String pattern) {
if (list == null || list.isEmpty()) {
return null;
}
List<String> result = new ArrayList<>(list.size());
for (String item : list) {
if (Pattern.matches(pattern, item)) {
result.add(item);
}
}
return result;
}
}

@ -0,0 +1,56 @@
package org.spiderflow.core.executor.function;
import org.apache.commons.codec.digest.DigestUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
@Component
@Comment("MD5常用方法")
public class MD5FunctionExecutor implements FunctionExecutor {
@Override
public String getFunctionPrefix() {
return "md5";
}
@Comment("md5加密")
@Example("${md5.string(resp.html)}")
public static String string(String str){
return DigestUtils.md5Hex(str);
}
@Comment("md5加密")
@Example("${md5.string(resp.bytes)}")
public static String string(byte[] bytes){
return DigestUtils.md5Hex(bytes);
}
@Comment("md5加密")
@Example("${md5.string(resp.stream)}")
public static String string(InputStream stream) throws IOException {
return DigestUtils.md5Hex(stream);
}
@Comment("md5加密")
@Example("${md5.bytes(resp.html)}")
public static byte[] bytes(String str){
return DigestUtils.md5(str);
}
@Comment("md5加密")
@Example("${md5.bytes(resp.bytes)}")
public static byte[] bytes(byte[] bytes){
return DigestUtils.md5(bytes);
}
@Comment("md5加密")
@Example("${md5.bytes(resp.stream)}")
public static byte[] bytes(InputStream stream) throws IOException {
return DigestUtils.md5(stream);
}
}

@ -0,0 +1,60 @@
package org.spiderflow.core.executor.function;
import org.apache.commons.lang3.RandomUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* /
* @author Administrator
*
*/
@Component
public class RandomFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "random";
}
@Comment("随机获取int")
@Example("${random.randomInt(1,10)}")
public static int randomInt(int min,int max){
return RandomUtils.nextInt(min, max);
}
@Comment("随机获取double")
@Example("${random.randomDouble(1,10)}")
public static double randomDouble(double min,double max){
return RandomUtils.nextDouble(min, max);
}
@Comment("随机获取long")
@Example("${random.randomLong(1,10)}")
public static long randomLong(long min,long max){
return RandomUtils.nextLong(min, max);
}
/**
*
* @param chars
* @param length
* @return String
*/
@Comment("随机获取字符串")
@Example("${random.string('abcde',10)}")
public static String string(String chars,int length){
if (chars != null) {
char[] newChars = new char[length];
int len = chars.length();
for (int i = 0; i < length; i++) {
newChars[i] = chars.charAt(randomInt(0,len));
}
return new String(newChars);
}
return null;
}
}

@ -0,0 +1,176 @@
package org.spiderflow.core.executor.function;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* String NPE
* @author Administrator
*
*/
@Component
@Comment("string常用方法")
public class StringFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "string";
}
@Comment("截取字符串方法")
@Example("${string.substring(str,5)}")
public static String substring(String content, int beginIndex) {
return content != null ? content.substring(beginIndex) : null;
}
@Comment("截取字符串方法")
@Example("${string.substring(str,0,str.length() - 1)}")
public static String substring(String content, int beginIndex, int endIndex) {
return content != null ? content.substring(beginIndex, endIndex) : null;
}
@Comment("将字符串转为小写")
@Example("${string.lower(str)}")
public static String lower(String content) {
return content != null ? content.toLowerCase() : null;
}
@Comment("将字符串转为大写")
@Example("${string.upper(str)}")
public static String upper(String content) {
return content != null ? content.toUpperCase() : null;
}
@Comment("查找指定字符在字符串在中的位置")
@Example("${string.indexOf(content,str)}")
public static int indexOf(String content, String str) {
return content != null ? content.indexOf(str) : -1;
}
@Comment("查找指定字符在字符串中最后出现的位置")
@Example("${string.lastIndexOf(content,str)}")
public static int lastIndexOf(String content, String str) {
return content != null ? content.lastIndexOf(str) : -1;
}
@Comment("查找指定字符在字符串在中的位置")
@Example("${string.indexOf(content,str,fromIndex)}")
public static int indexOf(String content, String str, int fromIndex) {
return content != null ? content.indexOf(str, fromIndex) : -1;
}
@Comment("将字符串转为int")
@Example("${string.toInt(value)}")
public static int toInt(String value){
return Integer.parseInt(value);
}
@Comment("将字符串转为Integer")
@Example("${string.toInt(value,defaultValue)}")
public static Integer toInt(String value,Integer defaultValue){
try {
return Integer.parseInt(value);
} catch (Exception e) {
return defaultValue;
}
}
@Comment("字符串替换")
@Example("${string.replace(content,source,target)}")
public static String replace(String content,String source,String target){
return content != null ? content.replace(source, target): null;
}
@Comment("正则替换字符串")
@Example("${string.replaceAll(content,regx,target)}")
public static String replaceAll(String content,String regx,String target){
return content != null ? content.replaceAll(regx, target): null;
}
@Comment("正则替换字符串")
@Example("${string.replaceFirst(content,regx,target)}")
public static String replaceFirst(String content,String regx,String target){
return content != null ? content.replaceFirst(regx, target): null;
}
@Comment("正则替换字符串")
@Example("${string.length(content)}")
public static int length(String content){
return content != null ? content.length() : -1;
}
@Comment("去除字符串两边的空格")
@Example("${string.trim(content)}")
public static String trim(String content){
return content != null ? content.trim() : null;
}
@Comment("分割字符串")
@Example("${string.split(content,regx)}")
public static List<String> split(String content,String regx){
return content != null ? Arrays.asList(content.split(regx)) : new ArrayList<>(0);
}
@Comment("获取字符串的byte[]")
@Example("${string.bytes(content)}")
public static byte[] bytes(String content){
return content != null ? content.getBytes() : null;
}
@Comment("获取字符串的byte[]")
@Example("${string.bytes(content,charset)}")
public static byte[] bytes(String content,String charset){
try {
return content != null ? content.getBytes(charset) : null;
} catch (UnsupportedEncodingException e) {
return null;
}
}
@Comment("byte[]转String")
@Example("${string.newString(bytes)}")
public static String newString(byte[] bytes){
return bytes != null ? new String(bytes) : null;
}
@Comment("byte[]转String")
@Example("${string.newString(bytes,charset)}")
public static String newString(byte[] bytes,String charset){
try {
return bytes != null ? new String(bytes,charset) : null;
} catch (UnsupportedEncodingException e) {
return null;
}
}
@Comment("判断两个字符串是否相同")
@Example("${string.newString(bytes,charset)}")
public static boolean equals(String str1,String str2){
return str1 == null ? str2 == null : str1.equals(str2);
}
@Comment("生成UUID")
@Example("${string.uuid()}")
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
@Comment("生成多个UUID")
@Example("${string.uuid(size)}")
public static List<String> uuids(Integer size) {
List<String> ids = new ArrayList<String>();
for (int i = 0; i < size; i++) {
ids.add(UUID.randomUUID().toString().replace("-", ""));
}
return ids;
}
}

@ -0,0 +1,30 @@
package org.spiderflow.core.executor.function;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* Created on 2019-12-06
*
* @author Octopus
*/
@Component
@Comment("thread常用方法")
public class ThreadFunctionExecutor implements FunctionExecutor {
@Override
public String getFunctionPrefix() {
return "thread";
}
@Comment("线程休眠")
@Example("${thread.sleep(1000L)}")
public static void sleep(Long sleepTime){
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,95 @@
package org.spiderflow.core.executor.function;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExecutor;
import org.springframework.stereotype.Component;
/**
* url / (UTF-8) NPE
*/
@Component
public class UrlFunctionExecutor implements FunctionExecutor{
@Override
public String getFunctionPrefix() {
return "url";
}
@Comment("获取url参数")
@Example("${url.parameter('http://www.baidu.com/s?wd=spider-flow','wd')}")
public static String parameter(String url,String key){
return parameterMap(url).get(key);
}
@Comment("获取url全部参数")
@Example("${url.parameterMap('http://www.baidu.com/s?wd=spider-flow&abbr=sf')}")
public static Map<String,String> parameterMap(String url){
Map<String,String> map = new HashMap<String,String>();
int index = url.indexOf("?");
if(index != -1) {
String param = url.substring(index+1);
if(StringUtils.isNotBlank(param)) {
String[] params = param.split("&");
for (String item : params) {
String[] kv = item.split("=");
if(kv.length > 0) {
if(StringUtils.isNotBlank(kv[0])) {
String value = "";
if(StringUtils.isNotBlank(kv[1])) {
int kv1Index = kv[1].indexOf("#");
if(kv1Index != -1) {
value = kv[1].substring(0,kv1Index);
}else {
value = kv[1];
}
}
map.put(kv[0],value);
}
}
}
}
}
return map;
}
@Comment("url编码")
@Example("${url.encode('http://www.baidu.com/s?wd=spider-flow')}")
public static String encode(String url){
return encode(url,Charset.defaultCharset().name());
}
@Comment("url编码")
@Example("${url.encode('http://www.baidu.com/s?wd=spider-flow','UTF-8')}")
public static String encode(String url,String charset){
try {
return url != null ? URLEncoder.encode(url,charset) : null;
} catch (UnsupportedEncodingException e) {
return null;
}
}
@Comment("url解码")
@Example("${url.decode(strVar)}")
public static String decode(String url){
return decode(url,Charset.defaultCharset().name());
}
@Comment("url解码")
@Example("${url.decode(strVar,'UTF-8')}")
public static String decode(String url,String charset){
try {
return url != null ? URLDecoder.decode(url, charset) : null;
} catch (UnsupportedEncodingException e) {
return null;
}
}
}

@ -0,0 +1,44 @@
package org.spiderflow.core.executor.function.extension;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
@Component
public class ArrayFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return Object[].class;
}
@Comment("获取数组的长度")
@Example("${arrayVar.size()}")
public static int size(Object[] objs){
return objs.length;
}
@Comment("将数组拼接起来")
@Example("${arrayVar.join()}")
public static String join(Object[] objs,String separator){
return StringUtils.join(objs,separator);
}
@Comment("将数组用separator拼接起来")
@Example("${arrayVar.join('-')}")
public static String join(Object[] objs){
return StringUtils.join(objs);
}
@Comment("将数组转为List")
@Example("${arrayVar.toList()}")
public static List<?> toList(Object[] objs){
return Arrays.asList(objs);
}
}

@ -0,0 +1,30 @@
package org.spiderflow.core.executor.function.extension;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
@Component
public class DateFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return Date.class;
}
@Comment("格式化日期")
@Example("${dateVar.format()}")
public static String format(Date date){
return format(date, "yyyy-MM-dd HH:mm:ss");
}
@Comment("格式化日期")
@Example("${dateVar.format('yyyy-MM-dd HH:mm:ss')}")
public static String format(Date date,String pattern){
return DateFormatUtils.format(date,pattern);
}
}

@ -0,0 +1,102 @@
package org.spiderflow.core.executor.function.extension;
import java.util.List;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.annotation.Return;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
@Component
public class ElementFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return Element.class;
}
@Comment("根据xpath提取内容")
@Example("${elementVar.xpath('//title/text()')}")
@Return({Element.class,String.class})
public static String xpath(Element element,String xpath){
return ExtractUtils.getValueByXPath(element, xpath);
}
@Comment("根据xpath提取内容")
@Example("${elementVar.xpaths('//h2/text()')}")
@Return({Element.class,String.class})
public static List<String> xpaths(Element element,String xpath){
return ExtractUtils.getValuesByXPath(element, xpath);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regx('<title>(.*?)</title>')}")
public static String regx(Element element,String regx){
return ExtractUtils.getFirstMatcher(element.html(), regx, true);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regx('<title>(.*?)</title>',1)}")
public static String regx(Element element,String regx,int groupIndex){
return ExtractUtils.getFirstMatcher(element.html(), regx, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regx('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<String> regx(Element element,String regx,List<Integer> groups){
return ExtractUtils.getFirstMatcher(element.html(), regx, groups);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regxs('<h2>(.*?)</h2>')}")
public static List<String> regxs(Element element,String regx){
return ExtractUtils.getMatchers(element.html(), regx, true);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regxs('<h2>(.*?)</h2>',1)}")
public static List<String> regxs(Element element,String regx,int groupIndex){
return ExtractUtils.getMatchers(element.html(), regx, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${elementVar.regxs('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<List<String>> regxs(Element element,String regx,List<Integer> groups){
return ExtractUtils.getMatchers(element.html(), regx, groups);
}
@Comment("根据css选择器提取内容")
@Example("${elementVar.selector('div > a')}")
public static Element selector(Element element,String cssQuery){
return element.selectFirst(cssQuery);
}
@Comment("根据css选择器提取内容")
@Example("${elementVar.selectors('div > a')}")
public static Elements selectors(Element element,String cssQuery){
return element.select(cssQuery);
}
@Comment("获取同级节点")
@Example("${elementVar.subling()}")
public static Elements subling(Element element){
return element.siblingElements();
}
@Comment("获取上级节点")
@Example("${elementVar.parent()}")
public static Element parent(Element element){
return element.parent();
}
@Comment("获取上级节点")
@Example("${elementVar.parents()}")
public static Elements parents(Element element){
return element.parents();
}
}

@ -0,0 +1,162 @@
package org.spiderflow.core.executor.function.extension;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class ElementsFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return Elements.class;
}
@Comment("根据xpath提取内容")
@Example("${elementsVar.xpath('//title/text()')}")
public static String xpath(Elements elements,String xpath){
return ExtractUtils.getValueByXPath(elements, xpath);
}
@Comment("根据xpath提取内容")
@Example("${elementsVar.xpaths('//h2/text()')}")
public static List<String> xpaths(Elements elements,String xpath){
return ExtractUtils.getValuesByXPath(elements, xpath);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regx('<title>(.*?)</title>')}")
public static String regx(Elements elements,String regx){
return ExtractUtils.getFirstMatcher(elements.html(), regx, true);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regx('<title>(.*?)</title>',1)}")
public static String regx(Elements elements,String regx,int groupIndex){
return ExtractUtils.getFirstMatcher(elements.html(), regx, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regx('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<String> regx(Elements elements,String regx,List<Integer> groups){
return ExtractUtils.getFirstMatcher(elements.html(), regx, groups);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regxs('<h2>(.*?)</h2>')}")
public static List<String> regxs(Elements elements,String regx){
return ExtractUtils.getMatchers(elements.html(), regx, true);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regxs('<h2>(.*?)</h2>',1)}")
public static List<String> regxs(Elements elements,String regx,int groupIndex){
return ExtractUtils.getMatchers(elements.html(), regx, groupIndex);
}
@Comment("根据正则表达式提取内容")
@Example("${elementsVar.regxs('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<List<String>> regxs(Elements elements,String regx,List<Integer> groups){
return ExtractUtils.getMatchers(elements.html(), regx, groups);
}
@Comment("根据css选择器提取内容")
@Example("${elementsVar.selector('div > a')}")
public static Element selector(Elements elements,String selector){
Elements foundElements = elements.select(selector);
if(foundElements.size() > 0){
return foundElements.get(0);
}
return null;
}
@Comment("返回所有attr")
@Example("${elementsVar.attrs('href')}")
public static List<String> attrs(Elements elements,String key){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.attr(key));
}
return list;
}
@Comment("返回所有value")
@Example("${elementsVar.vals()}")
public static List<String> vals(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.val());
}
return list;
}
@Comment("返回所有text")
@Example("${elementsVar.texts()}")
public static List<String> texts(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.text());
}
return list;
}
@Comment("返回所有html")
@Example("${elementsVar.htmls()}")
public static List<String> htmls(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.html());
}
return list;
}
@Comment("返回所有outerHtml")
@Example("${elementsVar.outerHtmls()}")
public static List<String> outerHtmls(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.outerHtml());
}
return list;
}
@Comment("返回所有ownTexts")
@Example("${elementsVar.ownTexts()}")
public static List<String> ownTexts(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.ownText());
}
return list;
}
@Comment("返回所有wholeText")
@Example("${elementsVar.wholeTexts()}")
public static List<String> wholeTexts(Elements elements){
List<String> list = new ArrayList<>(elements.size());
for (Element element : elements) {
list.add(element.wholeText());
}
return list;
}
@Comment("根据css选择器提取内容")
@Example("${elementsVar.selectors('div > a')}")
public static Elements selectors(Elements elements,String selector){
return elements.select(selector);
}
@Comment("获取上级节点")
@Example("${elementsVar.parents()}")
public static Elements parents(Elements elements){
return elements.parents();
}
}

@ -0,0 +1,57 @@
package org.spiderflow.core.executor.function.extension;
import org.apache.commons.lang3.StringUtils;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
@Component
public class ListFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return List.class;
}
@Comment("获取list的长度")
@Example("${listVar.length()}")
public static int length(List<?> list){
return list.size();
}
@Comment("将list拼接起来")
@Example("${listVar.join()}")
public static String join(List<?> list){
return StringUtils.join(list.toArray());
}
@Comment("将list用separator拼接起来")
@Example("${listVar.join('-')}")
public static String join(List<?> list,String separator){
if(list.size() == 1){
return list.get(0).toString();
}else{
return StringUtils.join(list.toArray(),separator);
}
}
@Comment("将list<String>排序")
@Example("${listVar.sort()}")
public static List<String> sort(List<String> list){
Collections.sort(list);
return list;
}
@Comment("将list打乱顺序")
@Example("${listVar.shuffle()}")
public static List<?> shuffle(List<?> list){
Collections.shuffle(list);
return list;
}
}

@ -0,0 +1,25 @@
package org.spiderflow.core.executor.function.extension;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class MapFunctionExtension implements FunctionExtension {
@Override
public Class<?> support() {
return Map.class;
}
@Comment("将map转换为List")
@Example("${mapmVar.toList('=')}")
public static List<String> toList(Map<?,?> map,String separator){
return map.entrySet().stream().map(entry-> entry.getKey() + separator + entry.getValue()).collect(Collectors.toList());
}
}

@ -0,0 +1,48 @@
package org.spiderflow.core.executor.function.extension;
import java.util.Objects;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
@Component
public class ObjectFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return Object.class;
}
@Comment("将对象转为string类型")
@Example("${objVar.string()}")
public static String string(Object obj){
if (obj instanceof String) {
return (String) obj;
}
return Objects.toString(obj);
}
@Comment("根据jsonpath提取内容")
@Example("${objVar.jsonpath('$.code')}")
public static Object jsonpath(Object obj,String path){
if(obj instanceof String){
return ExtractUtils.getValueByJsonPath(JSON.parse((String)obj), path);
}
return ExtractUtils.getValueByJsonPath(obj, path);
}
@Comment("睡眠等待一段时间")
@Example("${objVar.sleep(1000)}")
public static Object sleep(Object obj, int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
}
return obj;
}
}

@ -0,0 +1,127 @@
package org.spiderflow.core.executor.function.extension;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.annotation.Return;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExtension;
import org.spiderflow.io.SpiderResponse;
import org.springframework.stereotype.Component;
@Component
public class ResponseFunctionExtension implements FunctionExtension {
@Override
public Class<?> support() {
return SpiderResponse.class;
}
@Comment("将请求结果转为Element对象")
@Example("${resp.element()}")
public static Element element(SpiderResponse response) {
return Jsoup.parse(response.getHtml(),response.getUrl());
}
@Comment("根据xpath在请求结果中查找")
@Example("${resp.xpath('//title/text()')}")
@Return({Element.class, String.class})
public static String xpath(SpiderResponse response, String xpath) {
return ExtractUtils.getValueByXPath(element(response), xpath);
}
@Comment("根据xpath在请求结果中查找")
@Example("${resp.xpaths('//a/@href')}")
public static List<String> xpaths(SpiderResponse response, String xpath) {
return ExtractUtils.getValuesByXPath(element(response), xpath);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regx('<title>(.*?)</title>')}")
public static String regx(SpiderResponse response, String pattern) {
return ExtractUtils.getFirstMatcher(response.getHtml(), pattern, true);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regx('<title>(.*?)</title>',1)}")
public static String regx(SpiderResponse response, String pattern, int groupIndex) {
return ExtractUtils.getFirstMatcher(response.getHtml(), pattern, groupIndex);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regx('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<String> regx(SpiderResponse response, String pattern, List<Integer> groups) {
return ExtractUtils.getFirstMatcher(response.getHtml(), pattern, groups);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regxs('<h2>(.*?)</h2>')}")
public static List<String> regxs(SpiderResponse response, String pattern) {
return ExtractUtils.getMatchers(response.getHtml(), pattern, true);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regxs('<h2>(.*?)</h2>',1)}")
public static List<String> regxs(SpiderResponse response, String pattern, int groupIndex) {
return ExtractUtils.getMatchers(response.getHtml(), pattern, groupIndex);
}
@Comment("根据正则表达式提取请求结果中的内容")
@Example("${resp.regxs('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<List<String>> regxs(SpiderResponse response, String pattern, List<Integer> groups) {
return ExtractUtils.getMatchers(response.getHtml(), pattern, groups);
}
@Comment("根据css选择器提取请求结果")
@Example("${resp.selector('div > a')}")
public static Element selector(SpiderResponse response, String selector) {
return ElementFunctionExtension.selector(element(response), selector);
}
@Comment("根据css选择器提取请求结果")
@Example("${resp.selectors('div > a')}")
public static Elements selectors(SpiderResponse response, String selector) {
return ElementFunctionExtension.selectors(element(response), selector);
}
@Comment("根据jsonpath提取请求结果")
@Example("${resp.jsonpath('$.code')}")
public static Object jsonpath(SpiderResponse response, String path) {
return ExtractUtils.getValueByJsonPath(response.getJson(), path);
}
@Comment("获取页面上的链接")
@Example("${resp.links()}")
public static List<String> links(SpiderResponse response) {
return ExtractUtils.getAttrBySelector(element(response), "a", "abs:href")
.stream()
.filter(link -> StringUtils.isNotBlank(link))
.collect(Collectors.toList());
}
@Comment("获取页面上的链接")
@Example("${resp.links('https://www\\.xxx\\.com/xxxx/(.*?)')}")
public static List<String> links(SpiderResponse response, String regx) {
Pattern pattern = Pattern.compile(regx);
return links(response)
.stream()
.filter(link -> pattern.matcher(link).matches())
.collect(Collectors.toList());
}
@Comment("获取当前页面所有图片链接")
@Example("${resp.images()}")
public static List<String> images(SpiderResponse response) {
return ExtractUtils.getAttrBySelector(element(response), "img", "src")
.stream()
.filter(link -> StringUtils.isNotBlank(link))
.collect(Collectors.toList());
}
}

@ -0,0 +1,40 @@
package org.spiderflow.core.executor.function.extension;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.spiderflow.annotation.Example;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class SqlRowSetExtension implements FunctionExtension {
public static Map<String, String[]> tableMetaMap = new HashMap<>();
@Override
public Class<?> support() {
return SqlRowSet.class;
}
@Example("${rs.nextToMap()}")
public static Map<String, Object> nextToMap(SqlRowSet sqlRowSet) {
try {
if (!sqlRowSet.next()) {
return null;
}
String[] columnNames = sqlRowSet.getMetaData().getColumnNames();
Map<String, Object> result = new HashMap<>();
for (String columnName : columnNames) {
result.put(columnName, sqlRowSet.getObject(columnName));
}
return result;
} catch (Exception e) {
ExceptionUtils.wrapAndThrow(e);
}
return null;
}
}

@ -0,0 +1,143 @@
package org.spiderflow.core.executor.function.extension;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;
import org.spiderflow.annotation.Comment;
import org.spiderflow.annotation.Example;
import org.spiderflow.annotation.Return;
import org.spiderflow.core.executor.function.DateFunctionExecutor;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
@Component
public class StringFunctionExtension implements FunctionExtension{
@Override
public Class<?> support() {
return String.class;
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regx('<title>(.*?)</title>')}")
public static String regx(String source,String pattern){
return ExtractUtils.getFirstMatcher(source, pattern, true);
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regx('<title>(.*?)</title>',1)}")
public static String regx(String source,String pattern,int groupIndex){
return ExtractUtils.getFirstMatcher(source, pattern, groupIndex);
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regx('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<String> regx(String source,String pattern,List<Integer> groups){
return ExtractUtils.getFirstMatcher(source, pattern, groups);
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regxs('<h2>(.*?)</h2>')}")
public static List<String> regxs(String source,String pattern){
return ExtractUtils.getMatchers(source, pattern, true);
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regxs('<h2>(.*?)</h2>',1)}")
public static List<String> regxs(String source,String pattern,int groupIndex){
return ExtractUtils.getMatchers(source, pattern, groupIndex);
}
@Comment("根据正则表达式提取String中的内容")
@Example("${strVar.regxs('<a href=\"(.*?)\">(.*?)</a>',[1,2])}")
public static List<List<String>> regxs(String source,String pattern,List<Integer> groups){
return ExtractUtils.getMatchers(source, pattern, groups);
}
@Comment("根据xpath在String变量中查找")
@Example("${strVar.xpath('//title/text()')}")
@Return({Element.class,String.class})
public static String xpath(String source,String xpath){
return ExtractUtils.getValueByXPath(element(source), xpath);
}
@Comment("根据xpath在String变量中查找")
@Example("${strVar.xpaths('//a/@href')}")
public static List<String> xpaths(String source,String xpath){
return ExtractUtils.getValuesByXPath(element(source), xpath);
}
@Comment("将String变量转为Element对象")
@Example("${strVar.element()}")
public static Element element(String source){
return Parser.xmlParser().parseInput(source,"");
}
@Comment("根据css选择器提取")
@Example("${strVar.selector('div > a')}")
public static Element selector(String source,String cssQuery){
return element(source).selectFirst(cssQuery);
}
@Comment("根据css选择器提取")
@Example("${strVar.selector('div > a')}")
public static Elements selectors(String source,String cssQuery){
return element(source).select(cssQuery);
}
@Comment("将string转为json对象")
@Example("${strVar.json()}")
public static Object json(String source){
return JSON.parse(source);
}
@Comment("根据jsonpath提取内容")
@Example("${strVar.jsonpath('$.code')}")
public static Object jsonpath(String source,String jsonPath){
return ExtractUtils.getValueByJsonPath(json(source), jsonPath);
}
@Comment("将字符串转为int类型")
@Example("${strVar.toInt(0)}")
public static Integer toInt(String source,int defaultValue){
return NumberUtils.toInt(source, defaultValue);
}
@Comment("将字符串转为int类型")
@Example("${strVar.toInt()}")
public static Integer toInt(String source){
return NumberUtils.toInt(source);
}
@Comment("将字符串转为double类型")
@Example("${strVar.toDouble()}")
public static Double toDouble(String source){
return NumberUtils.toDouble(source);
}
@Comment("将字符串转为long类型")
@Example("${strVar.toLong()}")
public static Long toLong(String source){
return NumberUtils.toLong(source);
}
@Comment("将字符串转为date类型")
@Example("${strVar.toDate('yyyy-MM-dd HH:mm:ss')}")
public static Date toDate(String source,String pattern) throws ParseException{
return DateFunctionExecutor.parse(source, pattern);
}
@Comment("反转义字符串")
@Example("${strVar.unescape()}")
public static String unescape(String source){
return StringEscapeUtils.unescapeJava(source);
}
}

@ -0,0 +1,23 @@
package org.spiderflow.core.executor.shape;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class CommentExecutor implements ShapeExecutor{
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
}
@Override
public String supportShape() {
return "comment";
}
}

@ -0,0 +1,223 @@
package org.spiderflow.core.executor.shape;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.Grammerable;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.utils.DataSourceUtils;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.core.utils.ExtractUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.Grammer;
import org.spiderflow.model.SpiderNode;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Array;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.*;
/**
* SQL
*
* @author jmxd
*/
@Component
public class ExecuteSQLExecutor implements ShapeExecutor, Grammerable {
public static final String DATASOURCE_ID = "datasourceId";
public static final String SQL = "sql";
public static final String STATEMENT_TYPE = "statementType";
public static final String STATEMENT_SELECT = "select";
public static final String STATEMENT_SELECT_ONE = "selectOne";
public static final String STATEMENT_SELECT_INT = "selectInt";
public static final String STATEMENT_INSERT = "insert";
public static final String STATEMENT_UPDATE = "update";
public static final String STATEMENT_DELETE = "delete";
public static final String SELECT_RESULT_STREAM = "isStream";
public static final String STATEMENT_INSERT_PK = "insertofPk";
private static final Logger logger = LoggerFactory.getLogger(ExecuteSQLExecutor.class);
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String, Object> variables) {
String dsId = node.getStringJsonValue(DATASOURCE_ID);
String sql = node.getStringJsonValue(SQL);
if (StringUtils.isBlank(dsId)) {
logger.warn("数据源ID为空");
} else if (StringUtils.isBlank(sql)) {
logger.warn("sql为空");
} else {
JdbcTemplate template = new JdbcTemplate(DataSourceUtils.getDataSource(dsId));
//把变量替换成占位符
List<String> parameters = ExtractUtils.getMatchers(sql, "#(.*?)#", true);
sql = sql.replaceAll("#(.*?)#", "?");
try {
Object sqlObject = ExpressionUtils.execute(sql, variables);
if(sqlObject == null){
logger.warn("获取的sql为空");
return;
}
sql = sqlObject.toString();
context.pause(node.getNodeId(),"common",SQL,sql);
} catch (Exception e) {
logger.error("获取sql出错,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
int size = parameters.size();
Object[] params = new Object[size];
boolean hasList = false;
int parameterSize = 0;
//当参数中存在List或者数组时认为是批量操作
for (int i = 0; i < size; i++) {
Object parameter = ExpressionUtils.execute(parameters.get(i), variables);
if (parameter != null) {
if (parameter instanceof List) {
hasList = true;
parameterSize = Math.max(parameterSize, ((List<?>) parameter).size());
} else if (parameter.getClass().isArray()) {
hasList = true;
parameterSize = Math.max(parameterSize, Array.getLength(parameter));
}
}
params[i] = parameter;
}
String statementType = node.getStringJsonValue(STATEMENT_TYPE);
logger.debug("执行sql{}", sql);
if (STATEMENT_SELECT.equals(statementType)) {
boolean isStream = "1".equals(node.getStringJsonValue(SELECT_RESULT_STREAM));
try {
if (isStream) {
variables.put("rs", template.queryForRowSet(sql, params));
} else {
variables.put("rs", template.queryForList(sql, params));
}
} catch (Exception e) {
variables.put("rs", null);
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
} else if (STATEMENT_SELECT_ONE.equals(statementType)) {
Map<String, Object> rs;
try {
rs = template.queryForMap(sql, params);
variables.put("rs", rs);
} catch (Exception e) {
variables.put("rs", null);
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
} else if (STATEMENT_SELECT_INT.equals(statementType)) {
Integer rs;
try {
rs = template.queryForObject(sql, params, Integer.class);
rs = rs == null ? 0 : rs;
variables.put("rs", rs);
} catch (Exception e) {
variables.put("rs", 0);
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
} else if (STATEMENT_UPDATE.equals(statementType) || STATEMENT_INSERT.equals(statementType) || STATEMENT_DELETE.equals(statementType)) {
try {
int updateCount = 0;
if (hasList) {
/*
Object[]List<Object[]>
ListObject[]
List
*/
int[] rs = template.batchUpdate(sql, convertParameters(params, parameterSize));
if (rs.length > 0) {
updateCount = Arrays.stream(rs).sum();
}
} else {
updateCount = template.update(sql, params);
}
variables.put("rs", updateCount);
} catch (Exception e) {
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
variables.put("rs", -1);
ExceptionUtils.wrapAndThrow(e);
}
} else if(STATEMENT_INSERT_PK.equals(statementType)) {
try {
KeyHolder keyHolder = new GeneratedKeyHolder();
final String insertSQL = sql;
template.update(con -> {
PreparedStatement ps = con.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS);
new ArgumentPreparedStatementSetter(params).setValues(ps);
return ps;
}, keyHolder);
variables.put("rs", keyHolder.getKey().intValue());
} catch (Exception e) {
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
variables.put("rs", -1);
ExceptionUtils.wrapAndThrow(e);
}
}
}
}
private List<Object[]> convertParameters(Object[] params, int length) {
List<Object[]> result = new ArrayList<>(length);
int size = params.length;
for (int i = 0; i < length; i++) {
Object[] parameters = new Object[size];
for (int j = 0; j < size; j++) {
parameters[j] = getValue(params[j], i);
}
result.add(parameters);
}
return result;
}
private Object getValue(Object object, int index) {
if (object == null) {
return null;
} else if (object instanceof List) {
List<?> list = (List<?>) object;
int size = list.size();
if (size > 0) {
return list.get(Math.min(list.size() - 1, index));
}
} else if (object.getClass().isArray()) {
int size = Array.getLength(object);
if (size > 0) {
Array.get(object, Math.min(-1, index));
}
} else {
return object;
}
return null;
}
@Override
public String supportShape() {
return "executeSql";
}
@Override
public List<Grammer> grammers() {
Grammer grammer = new Grammer();
grammer.setComment("执行SQL结果");
grammer.setFunction("rs");
grammer.setReturns(Arrays.asList("List<Map<String,Object>>", "int"));
return Collections.singletonList(grammer);
}
}

@ -0,0 +1,52 @@
package org.spiderflow.core.executor.shape;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
*
*
*/
@Component
public class ForkJoinExecutor implements ShapeExecutor {
/**
*
*/
private Map<String, Map<String, Object>> cachedVariables = new HashMap<>();
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String, Object> variables) {
}
@Override
public String supportShape() {
return "forkJoin";
}
@Override
public boolean allowExecuteNext(SpiderNode node, SpiderContext context, Map<String, Object> variables) {
String key = context.getId() + "-" + node.getNodeId();
synchronized (node){
boolean isDone = node.isDone();
Map<String, Object> cached = cachedVariables.get(key);
if(!isDone){
if(cached == null){
cached = new HashMap<>();
cachedVariables.put(key, cached);
}
cached.putAll(variables);
}else if(cached != null){
//将缓存的变量存入到当前变量中,传递给下一级
variables.putAll(cached);
cachedVariables.remove(key);
}
return isDone;
}
}
}

@ -0,0 +1,50 @@
package org.spiderflow.core.executor.shape;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class FunctionExecutor implements ShapeExecutor{
public static final String FUNCTION = "function";
private static final Logger logger = LoggerFactory.getLogger(FunctionExecutor.class);
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
List<Map<String, String>> functions = node.getListJsonValue(FUNCTION);
for (Map<String, String> item : functions) {
String function = item.get(FUNCTION);
if(StringUtils.isNotBlank(function)){
try {
logger.debug("执行函数{}",function);
ExpressionUtils.execute(function, variables);
} catch (Exception e) {
logger.error("执行函数{}失败,异常信息:{}",function,e);
ExceptionUtils.wrapAndThrow(e);
}
}
}
}
@Override
public String supportShape() {
return "function";
}
}

@ -0,0 +1,32 @@
package org.spiderflow.core.executor.shape;
import java.util.Map;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class LoopExecutor implements ShapeExecutor{
public static final String LOOP_ITEM = "loopItem";
public static final String LOOP_START = "loopStart";
public static final String LOOP_END = "loopEnd";
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
}
@Override
public String supportShape() {
return "loop";
}
}

@ -0,0 +1,235 @@
package org.spiderflow.core.executor.shape;
import com.alibaba.fastjson.JSON;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.ibatis.jdbc.SQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.serializer.FastJsonSerializer;
import org.spiderflow.core.utils.DataSourceUtils;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.io.SpiderResponse;
import org.spiderflow.listener.SpiderListener;
import org.spiderflow.model.SpiderNode;
import org.spiderflow.model.SpiderOutput;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.*;
/**
*
* @author Administrator
*
*/
@Component
public class OutputExecutor implements ShapeExecutor, SpiderListener {
public static final String OUTPUT_ALL = "output-all";
public static final String OUTPUT_NAME = "output-name";
public static final String OUTPUT_VALUE = "output-value";
public static final String DATASOURCE_ID = "datasourceId";
public static final String OUTPUT_DATABASE = "output-database";
public static final String OUTPUT_CSV = "output-csv";
public static final String TABLE_NAME = "tableName";
public static final String CSV_NAME = "csvName";
public static final String CSV_ENCODING = "csvEncoding";
private static Logger logger = LoggerFactory.getLogger(OutputExecutor.class);
/**
* CSVPrinter
*/
private Map<String, CSVPrinter> cachePrinter = new HashMap<>();
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
SpiderOutput output = new SpiderOutput();
output.setNodeName(node.getNodeName());
output.setNodeId(node.getNodeId());
boolean outputAll = "1".equals(node.getStringJsonValue(OUTPUT_ALL));
boolean databaseFlag = "1".equals(node.getStringJsonValue(OUTPUT_DATABASE));
boolean csvFlag = "1".equals(node.getStringJsonValue(OUTPUT_CSV));
if (outputAll) {
outputAll(output, variables);
}
List<Map<String, String>> outputs = node.getListJsonValue(OUTPUT_NAME, OUTPUT_VALUE);
Map<String, Object> outputData = null;
if (databaseFlag || csvFlag) {
outputData = new HashMap<>(outputs.size());
}
for (Map<String, String> item : outputs) {
Object value = null;
String outputValue = item.get(OUTPUT_VALUE);
String outputName = item.get(OUTPUT_NAME);
try {
value = ExpressionUtils.execute(outputValue, variables);
context.pause(node.getNodeId(),"common",outputName,value);
logger.debug("输出{}={}", outputName,value);
} catch (Exception e) {
logger.error("输出{}出错,异常信息:{}", outputName,e);
}
output.addOutput(outputName, value);
if ((databaseFlag || csvFlag) && value != null) {
outputData.put(outputName, value.toString());
}
}
if(databaseFlag){
String dsId = node.getStringJsonValue(DATASOURCE_ID);
String tableName = node.getStringJsonValue(TABLE_NAME);
if (StringUtils.isBlank(dsId)) {
logger.warn("数据源ID为空");
} else if (StringUtils.isBlank(tableName)) {
logger.warn("表名为空!");
} else {
outputDB(dsId, tableName, outputData);
}
}
if (csvFlag) {
String csvName = node.getStringJsonValue(CSV_NAME);
outputCSV(node, context, csvName, outputData);
}
context.addOutput(output);
}
/**
*
* @param output
* @param variables
*/
private void outputAll(SpiderOutput output,Map<String,Object> variables){
for (Map.Entry<String, Object> item : variables.entrySet()) {
Object value = item.getValue();
if (value instanceof SpiderResponse) {
SpiderResponse resp = (SpiderResponse) value;
output.addOutput(item.getKey() + ".html", resp.getHtml());
continue;
}
//去除不输出的信息
if ("ex".equals(item.getKey())) {
continue;
}
//去除不能序列化的参数
try {
JSON.toJSONString(value, FastJsonSerializer.serializeConfig);
} catch (Exception e) {
e.printStackTrace();
continue;
}
//输出信息
output.addOutput(item.getKey(), item.getValue());
}
}
private void outputDB(String databaseId, String tableName, Map<String, Object> data) {
if (data == null || data.isEmpty()) {
return;
}
JdbcTemplate template = new JdbcTemplate(DataSourceUtils.getDataSource(databaseId));
Set<String> keySet = data.keySet();
Object[] params = new Object[data.size()];
SQL sql = new SQL();
//设置表名
sql.INSERT_INTO(tableName);
int index = 0;
//设置字段名
for (String key : keySet) {
sql.VALUES(key, "?");
params[index] = data.get(key);
index++;
}
try {
//执行sql
template.update(sql.toString(), params);
} catch (Exception e) {
logger.error("执行sql出错,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
}
private void outputCSV(SpiderNode node, SpiderContext context, String csvName, Map<String, Object> data) {
if (data == null || data.isEmpty()) {
return;
}
String key = context.getId() + "-" + node.getNodeId();
CSVPrinter printer = cachePrinter.get(key);
List<String> records = new ArrayList<>(data.size());
String[] headers = data.keySet().toArray(new String[data.size()]);
try {
if (printer == null) {
synchronized (cachePrinter) {
printer = cachePrinter.get(key);
if (printer == null) {
CSVFormat format = CSVFormat.DEFAULT.withHeader(headers);
FileOutputStream os = new FileOutputStream(csvName);
String csvEncoding = node.getStringJsonValue(CSV_ENCODING);
if ("UTF-8BOM".equals(csvEncoding)) {
csvEncoding = csvEncoding.substring(0, 5);
byte[] bom = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
os.write(bom);
os.flush();
}
OutputStreamWriter osw = new OutputStreamWriter(os, csvEncoding);
printer = new CSVPrinter(osw, format);
cachePrinter.put(key, printer);
}
}
}
for (int i = 0; i < headers.length; i++) {
records.add(data.get(headers[i]).toString());
}
synchronized (cachePrinter) {
printer.printRecord(records);
}
} catch (IOException e) {
logger.error("文件输出错误,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
}
@Override
public String supportShape() {
return "output";
}
@Override
public void beforeStart(SpiderContext context) {
}
@Override
public void afterEnd(SpiderContext context) {
this.releasePrinters();
}
private void releasePrinters() {
for (Iterator<Map.Entry<String, CSVPrinter>> iterator = this.cachePrinter.entrySet().iterator(); iterator.hasNext(); ) {
Map.Entry<String, CSVPrinter> entry = iterator.next();
CSVPrinter printer = entry.getValue();
if (printer != null) {
try {
printer.flush();
printer.close();
this.cachePrinter.remove(entry.getKey());
} catch (IOException e) {
logger.error("文件输出错误,异常信息:{}", e.getMessage(), e);
ExceptionUtils.wrapAndThrow(e);
}
}
}
}
}

@ -0,0 +1,53 @@
package org.spiderflow.core.executor.shape;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.Spider;
import org.spiderflow.core.model.SpiderFlow;
import org.spiderflow.core.service.SpiderFlowService;
import org.spiderflow.core.utils.SpiderFlowUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class ProcessExecutor implements ShapeExecutor{
public static final String FLOW_ID = "flowId";
private static Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
@Autowired
private SpiderFlowService spiderFlowService;
@Autowired
private Spider spider;
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
String flowId = node.getStringJsonValue("flowId");
SpiderFlow spiderFlow = spiderFlowService.getById(flowId);
if(spiderFlow != null){
logger.info("执行子流程:{}", spiderFlow.getName());
SpiderNode root = SpiderFlowUtils.loadXMLFromString(spiderFlow.getXml());
spider.executeNode(null,root,context,variables);
}else{
logger.info("执行子流程:{}失败,找不到该子流程", flowId);
}
}
@Override
public String supportShape() {
return "process";
}
}

@ -0,0 +1,481 @@
package org.spiderflow.core.executor.shape;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.Grammerable;
import org.spiderflow.context.CookieContext;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.executor.function.MD5FunctionExecutor;
import org.spiderflow.core.io.HttpRequest;
import org.spiderflow.core.io.HttpResponse;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.io.SpiderResponse;
import org.spiderflow.listener.SpiderListener;
import org.spiderflow.model.Grammer;
import org.spiderflow.model.SpiderNode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
/**
*
* @author Administrator
*
*/
@Component
public class RequestExecutor implements ShapeExecutor,Grammerable, SpiderListener {
public static final String SLEEP = "sleep";
public static final String URL = "url";
public static final String PROXY = "proxy";
public static final String REQUEST_METHOD = "method";
public static final String PARAMETER_NAME = "parameter-name";
public static final String PARAMETER_VALUE = "parameter-value";
public static final String COOKIE_NAME = "cookie-name";
public static final String COOKIE_VALUE = "cookie-value";
public static final String PARAMETER_FORM_NAME = "parameter-form-name";
public static final String PARAMETER_FORM_VALUE = "parameter-form-value";
public static final String PARAMETER_FORM_FILENAME = "parameter-form-filename";
public static final String PARAMETER_FORM_TYPE = "parameter-form-type";
public static final String BODY_TYPE = "body-type";
public static final String BODY_CONTENT_TYPE = "body-content-type";
public static final String REQUEST_BODY = "request-body";
public static final String HEADER_NAME = "header-name";
public static final String HEADER_VALUE = "header-value";
public static final String TIMEOUT = "timeout";
public static final String RETRY_COUNT = "retryCount";
public static final String RETRY_INTERVAL = "retryInterval";
public static final String RESPONSE_CHARSET = "response-charset";
public static final String FOLLOW_REDIRECT = "follow-redirect";
public static final String TLS_VALIDATE = "tls-validate";
public static final String LAST_EXECUTE_TIME = "__last_execute_time_";
public static final String COOKIE_AUTO_SET = "cookie-auto-set";
public static final String REPEAT_ENABLE = "repeat-enable";
public static final String BLOOM_FILTER_KEY = "_bloomfilter";
@Value("${spider.workspace}")
private String workspcace;
@Value("${spider.bloomfilter.capacity:5000000}")
private Integer capacity;
@Value("${spider.bloomfilter.error-rate:0.00001}")
private Double errorRate;
private static final Logger logger = LoggerFactory.getLogger(RequestExecutor.class);
@Override
public String supportShape() {
return "request";
}
@PostConstruct
void init(){
//允许设置被限制的请求头
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
}
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
CookieContext cookieContext = context.getCookieContext();
String sleepCondition = node.getStringJsonValue(SLEEP);
if(StringUtils.isNotBlank(sleepCondition)){
try {
Object value = ExpressionUtils.execute(sleepCondition, variables);
if(value != null){
long sleepTime = NumberUtils.toLong(value.toString(), 0L);
synchronized (node.getNodeId().intern()) {
//实际等待时间 = 上次执行时间 + 睡眠时间 - 当前时间
Long lastExecuteTime = context.get(LAST_EXECUTE_TIME + node.getNodeId(), 0L);
if (lastExecuteTime != 0) {
sleepTime = lastExecuteTime + sleepTime - System.currentTimeMillis();
}
if (sleepTime > 0) {
context.pause(node.getNodeId(),"common",SLEEP,sleepTime);
logger.debug("设置延迟时间:{}ms", sleepTime);
Thread.sleep(sleepTime);
}
context.put(LAST_EXECUTE_TIME + node.getNodeId(), System.currentTimeMillis());
}
}
} catch (Throwable t) {
logger.error("设置延迟时间失败", t);
}
}
BloomFilter<String> bloomFilter = null;
//重试次数
int retryCount = NumberUtils.toInt(node.getStringJsonValue(RETRY_COUNT), 0) + 1;
//重试间隔时间,单位毫秒
int retryInterval = NumberUtils.toInt(node.getStringJsonValue(RETRY_INTERVAL), 0);
boolean successed = false;
for (int i = 0; i < retryCount && !successed; i++) {
HttpRequest request = HttpRequest.create();
//设置请求url
String url = null;
try {
url = ExpressionUtils.execute(node.getStringJsonValue(URL), variables).toString();
} catch (Exception e) {
logger.error("设置请求url出错异常信息", e);
ExceptionUtils.wrapAndThrow(e);
}
if("1".equalsIgnoreCase(node.getStringJsonValue(REPEAT_ENABLE,"0"))){
bloomFilter = createBloomFilter(context);
synchronized (bloomFilter){
if(bloomFilter.mightContain(MD5FunctionExecutor.string(url))){
logger.info("过滤重复URL:{}",url);
return;
}
}
}
context.pause(node.getNodeId(),"common",URL,url);
logger.info("设置请求url:{}", url);
request.url(url);
//设置请求超时时间
int timeout = NumberUtils.toInt(node.getStringJsonValue(TIMEOUT), 60000);
logger.debug("设置请求超时时间:{}", timeout);
request.timeout(timeout);
String method = Objects.toString(node.getStringJsonValue(REQUEST_METHOD), "GET");
//设置请求方法
request.method(method);
logger.debug("设置请求方法:{}", method);
//是否跟随重定向
boolean followRedirects = !"0".equals(node.getStringJsonValue(FOLLOW_REDIRECT));
request.followRedirect(followRedirects);
logger.debug("设置跟随重定向:{}", followRedirects);
//是否验证TLS证书,默认是验证
if("0".equals(node.getStringJsonValue(TLS_VALIDATE))){
request.validateTLSCertificates(false);
logger.debug("设置TLS证书验证{}", false);
}
SpiderNode root = context.getRootNode();
//设置请求header
setRequestHeader(root, request, root.getListJsonValue(HEADER_NAME,HEADER_VALUE), context, variables);
setRequestHeader(node, request, node.getListJsonValue(HEADER_NAME,HEADER_VALUE), context, variables);
//设置全局Cookie
Map<String, String> cookies = getRequestCookie(root, root.getListJsonValue(COOKIE_NAME, COOKIE_VALUE), context, variables);
if(!cookies.isEmpty()){
logger.info("设置全局Cookie{}", cookies);
request.cookies(cookies);
}
//设置自动管理的Cookie
boolean cookieAutoSet = !"0".equals(node.getStringJsonValue(COOKIE_AUTO_SET));
if(cookieAutoSet && !cookieContext.isEmpty()){
context.pause(node.getNodeId(),COOKIE_AUTO_SET,COOKIE_AUTO_SET,cookieContext);
request.cookies(cookieContext);
logger.info("自动设置Cookie{}", cookieContext);
}
//设置本节点Cookie
cookies = getRequestCookie(node, node.getListJsonValue(COOKIE_NAME, COOKIE_VALUE), context, variables);
if(!cookies.isEmpty()){
request.cookies(cookies);
logger.debug("设置Cookie{}", cookies);
}
if(cookieAutoSet){
cookieContext.putAll(cookies);
}
String bodyType = node.getStringJsonValue(BODY_TYPE);
List<InputStream> streams = null;
if("raw".equals(bodyType)){
String contentType = node.getStringJsonValue(BODY_CONTENT_TYPE);
request.contentType(contentType);
try {
Object requestBody = ExpressionUtils.execute(node.getStringJsonValue(REQUEST_BODY), variables);
context.pause(node.getNodeId(),"request-body",REQUEST_BODY,requestBody);
request.data(requestBody);
logger.info("设置请求Body:{}", requestBody);
} catch (Exception e) {
logger.debug("设置请求Body出错", e);
}
}else if("form-data".equals(bodyType)){
List<Map<String, String>> formParameters = node.getListJsonValue(PARAMETER_FORM_NAME,PARAMETER_FORM_VALUE,PARAMETER_FORM_TYPE,PARAMETER_FORM_FILENAME);
streams = setRequestFormParameter(node,request,formParameters,context,variables);
}else{
//设置请求参数
setRequestParameter(root, request, root.getListJsonValue(PARAMETER_NAME,PARAMETER_VALUE), context, variables);
setRequestParameter(node, request, node.getListJsonValue(PARAMETER_NAME,PARAMETER_VALUE), context, variables);
}
//设置代理
String proxy = node.getStringJsonValue(PROXY);
if(StringUtils.isNotBlank(proxy)){
try {
Object value = ExpressionUtils.execute(proxy, variables);
context.pause(node.getNodeId(),"common",PROXY,value);
if(value != null){
String[] proxyArr = value.toString().split(":");
if(proxyArr.length == 2){
request.proxy(proxyArr[0], Integer.parseInt(proxyArr[1]));
logger.info("设置代理:{}",proxy);
}
}
} catch (Exception e) {
logger.error("设置代理出错,异常信息:{}",e);
}
}
Throwable exception = null;
try {
HttpResponse response = request.execute();
successed = response.getStatusCode() == 200;
if(successed){
if(bloomFilter != null){
synchronized (bloomFilter){
bloomFilter.put(MD5FunctionExecutor.string(url));
}
}
String charset = node.getStringJsonValue(RESPONSE_CHARSET);
if(StringUtils.isNotBlank(charset)){
response.setCharset(charset);
logger.debug("设置response charset:{}",charset);
}
//cookie存入cookieContext
cookieContext.putAll(response.getCookies());
//结果存入变量
variables.put("resp", response);
}
} catch (IOException e) {
successed = false;
exception = e;
} finally{
if(streams != null){
for (InputStream is : streams) {
try {
is.close();
} catch (Exception e) {
}
}
}
if(!successed){
if(i + 1 < retryCount){
if(retryInterval > 0){
try {
Thread.sleep(retryInterval);
} catch (InterruptedException ignored) {
}
}
logger.info("第{}次重试:{}",i + 1,url);
}else{
//记录访问失败的日志
if(context.getFlowId() != null){ //测试环境
//TODO 需增加记录请求参数
File file = new File(workspcace, context.getFlowId() + File.separator + "logs" + File.separator + "access_error.log");
try {
File directory = file.getParentFile();
if(!directory.exists()){
directory.mkdirs();
}
FileUtils.write(file,url + "\r\n","UTF-8",true);
} catch (IOException ignored) {
}
}
logger.error("请求{}出错,异常信息:{}",url,exception);
}
}
}
}
}
private List<InputStream> setRequestFormParameter(SpiderNode node, HttpRequest request,List<Map<String, String>> parameters,SpiderContext context,Map<String,Object> variables){
List<InputStream> streams = new ArrayList<>();
if(parameters != null){
for (Map<String,String> nameValue : parameters) {
Object value;
String parameterName = nameValue.get(PARAMETER_FORM_NAME);
if(StringUtils.isNotBlank(parameterName)){
String parameterValue = nameValue.get(PARAMETER_FORM_VALUE);
String parameterType = nameValue.get(PARAMETER_FORM_TYPE);
String parameterFilename = nameValue.get(PARAMETER_FORM_FILENAME);
boolean hasFile = "file".equals(parameterType);
try {
value = ExpressionUtils.execute(parameterValue, variables);
if(hasFile){
InputStream stream = null;
if(value instanceof byte[]){
stream = new ByteArrayInputStream((byte[]) value);
}else if(value instanceof String){
stream = new ByteArrayInputStream(((String)value).getBytes());
}else if(value instanceof InputStream){
stream = (InputStream) value;
}
if(stream != null){
streams.add(stream);
request.data(parameterName, parameterFilename, stream);
context.pause(node.getNodeId(),"request-body",parameterName,parameterFilename);
logger.info("设置请求参数:{}={}",parameterName,parameterFilename);
}else{
logger.warn("设置请求参数:{}失败,无二进制内容",parameterName);
}
}else{
request.data(parameterName, value);
context.pause(node.getNodeId(),"request-body",parameterName,value);
logger.info("设置请求参数:{}={}",parameterName,value);
}
} catch (Exception e) {
logger.error("设置请求参数:{}出错,异常信息:{}",parameterName,e);
}
}
}
}
return streams;
}
private Map<String,String> getRequestCookie(SpiderNode node, List<Map<String, String>> cookies, SpiderContext context, Map<String, Object> variables) {
Map<String,String> cookieMap = new HashMap<>();
if (cookies != null) {
for (Map<String, String> nameValue : cookies) {
Object value;
String cookieName = nameValue.get(COOKIE_NAME);
if (StringUtils.isNotBlank(cookieName)) {
String cookieValue = nameValue.get(COOKIE_VALUE);
try {
value = ExpressionUtils.execute(cookieValue, variables);
if (value != null) {
cookieMap.put(cookieName, value.toString());
context.pause(node.getNodeId(),"request-cookie",cookieName,value.toString());
logger.info("设置请求Cookie{}={}", cookieName, value);
}
} catch (Exception e) {
logger.error("设置请求Cookie{}出错,异常信息:{}", cookieName, e);
}
}
}
}
return cookieMap;
}
private void setRequestParameter(SpiderNode node, HttpRequest request, List<Map<String, String>> parameters, SpiderContext context, Map<String, Object> variables) {
if (parameters != null) {
for (Map<String, String> nameValue : parameters) {
Object value = null;
String parameterName = nameValue.get(PARAMETER_NAME);
if (StringUtils.isNotBlank(parameterName)) {
String parameterValue = nameValue.get(PARAMETER_VALUE);
try {
value = ExpressionUtils.execute(parameterValue, variables);
context.pause(node.getNodeId(),"request-parameter",parameterName,value);
logger.info("设置请求参数:{}={}", parameterName, value);
} catch (Exception e) {
logger.error("设置请求参数:{}出错,异常信息:{}", parameterName, e);
}
request.data(parameterName, value);
}
}
}
}
private void setRequestHeader(SpiderNode node,HttpRequest request, List<Map<String, String>> headers, SpiderContext context, Map<String, Object> variables) {
if (headers != null) {
for (Map<String, String> nameValue : headers) {
Object value = null;
String headerName = nameValue.get(HEADER_NAME);
if (StringUtils.isNotBlank(headerName)) {
String headerValue = nameValue.get(HEADER_VALUE);
try {
value = ExpressionUtils.execute(headerValue, variables);
context.pause(node.getNodeId(),"request-header",headerName,value);
logger.info("设置请求Header{}={}", headerName, value);
} catch (Exception e) {
logger.error("设置请求Header{}出错,异常信息:{}", headerName, e);
}
request.header(headerName, value);
}
}
}
}
@Override
public List<Grammer> grammers() {
List<Grammer> grammers = Grammer.findGrammers(SpiderResponse.class,"resp" , "SpiderResponse", false);
Grammer grammer = new Grammer();
grammer.setFunction("resp");
grammer.setComment("抓取结果");
grammer.setOwner("SpiderResponse");
grammers.add(grammer);
return grammers;
}
@Override
public void beforeStart(SpiderContext context) {
}
private BloomFilter<String> createBloomFilter(SpiderContext context){
BloomFilter<String> filter = context.get(BLOOM_FILTER_KEY);
if(filter == null){
Funnel<CharSequence> funnel = Funnels.stringFunnel(Charset.forName("UTF-8"));
String fileName = context.getFlowId() + File.separator + "url.bf";
File file = new File(workspcace,fileName);
if(file.exists()){
try(FileInputStream fis = new FileInputStream(file)){
filter = BloomFilter.readFrom(fis,funnel);
} catch (IOException e) {
logger.error("读取布隆过滤器出错",e);
}
}else{
filter = BloomFilter.create(funnel,capacity,errorRate);
}
context.put(BLOOM_FILTER_KEY,filter);
}
return filter;
}
@Override
public void afterEnd(SpiderContext context) {
BloomFilter<String> filter = context.get(BLOOM_FILTER_KEY);
if(filter != null){
File file = new File(workspcace,context.getFlowId() + File.separator + "url.bf");
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try(FileOutputStream fos = new FileOutputStream(file)){
filter.writeTo(fos);
fos.flush();
}catch(IOException e){
logger.error("保存布隆过滤器出错",e);
}
}
}
}

@ -0,0 +1,28 @@
package org.spiderflow.core.executor.shape;
import java.util.Map;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class StartExecutor implements ShapeExecutor{
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
}
@Override
public String supportShape() {
return "start";
}
}

@ -0,0 +1,58 @@
package org.spiderflow.core.executor.shape;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.core.utils.ExpressionUtils;
import org.spiderflow.executor.ShapeExecutor;
import org.spiderflow.model.SpiderNode;
import org.springframework.stereotype.Component;
/**
*
* @author Administrator
*
*/
@Component
public class VariableExecutor implements ShapeExecutor{
private static final String VARIABLE_NAME = "variable-name";
private static final String VARIABLE_VALUE = "variable-value";
private static final Logger logger = LoggerFactory.getLogger(VariableExecutor.class);
@Override
public void execute(SpiderNode node, SpiderContext context, Map<String,Object> variables) {
List<Map<String, String>> variableList = node.getListJsonValue(VARIABLE_NAME,VARIABLE_VALUE);
for (Map<String, String> nameValue : variableList) {
Object value = null;
String variableName = nameValue.get(VARIABLE_NAME);
String variableValue = nameValue.get(VARIABLE_VALUE);
try {
value = ExpressionUtils.execute(variableValue, variables);
logger.debug("设置变量{}={}",variableName,value);
context.pause(node.getNodeId(),"common",variableName,value);
} catch (Exception e) {
logger.error("设置变量{}出错,异常信息:{}",variableName,e);
ExceptionUtils.wrapAndThrow(e);
}
variables.put(variableName, value);
}
}
@Override
public String supportShape() {
return "variable";
}
@Override
public boolean isThread() {
return false;
}
}

@ -0,0 +1,52 @@
package org.spiderflow.core.expression;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
import org.spiderflow.ExpressionEngine;
import org.spiderflow.core.expression.interpreter.Reflection;
import org.spiderflow.executor.FunctionExecutor;
import org.spiderflow.executor.FunctionExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DefaultExpressionEngine implements ExpressionEngine{
@Autowired
private List<FunctionExecutor> functionExecutors;
@Autowired
private List<FunctionExtension> functionExtensions;
@PostConstruct
private void init(){
for (FunctionExtension extension : functionExtensions) {
Reflection.getInstance().registerExtensionClass(extension.support(), extension.getClass());
}
}
@Override
public Object execute(String expression, Map<String, Object> variables) {
if(StringUtils.isBlank(expression)){
return expression;
}
ExpressionTemplateContext context = new ExpressionTemplateContext(variables);
for (FunctionExecutor executor : functionExecutors) {
context.set(executor.getFunctionPrefix(), executor);
}
ExpressionGlobalVariables.getVariables().entrySet().forEach(entry->{
context.set(entry.getKey(),ExpressionTemplate.create(entry.getValue()).render(context));
});
try {
ExpressionTemplateContext.set(context);
return ExpressionTemplate.create(expression).render(context);
} finally {
ExpressionTemplateContext.remove();
}
}
}

@ -0,0 +1,118 @@
package org.spiderflow.core.expression;
import org.spiderflow.core.expression.parsing.Span;
import org.spiderflow.core.expression.parsing.Span.Line;
import org.spiderflow.core.expression.parsing.TokenStream;
/** All errors reported by the library go through the static functions of this class. */
public class ExpressionError {
/**
* <p>
* Create an error message based on the provided message and stream, highlighting the line on which the error happened. If the
* stream has more tokens, the next token will be highlighted. Otherwise the end of the source of the stream will be
* highlighted.
* </p>
*
* <p>
* Throws a {@link RuntimeException}
* </p>
*/
public static void error (String message, TokenStream stream) {
if (stream.hasMore())
error(message, stream.consume().getSpan());
else {
String source = stream.getSource();
if (source == null)
error(message, new Span(" ", 0, 1));
else
error(message, new Span(source, source.length() - 1, source.length()));
}
}
/** Create an error message based on the provided message and location, highlighting the location in the line on which the
* error happened. Throws a {@link TemplateException} **/
public static void error (String message, Span location, Throwable cause) {
Line line = location.getLine();
message = "Error (" + line.getLineNumber() + "): " + message + "\n\n";
message += line.getText();
message += "\n";
int errorStart = location.getStart() - line.getStart();
int errorEnd = errorStart + location.getText().length() - 1;
for (int i = 0, n = line.getText().length(); i < n; i++) {
boolean useTab = line.getText().charAt(i) == '\t';
message += i >= errorStart && i <= errorEnd ? "^" : useTab ? "\t" : " ";
}
if (cause == null)
throw new TemplateException(message, location);
else
throw new TemplateException(message, location, cause);
}
/** Create an error message based on the provided message and location, highlighting the location in the line on which the
* error happened. Throws a {@link TemplateException} **/
public static void error (String message, Span location) {
error(message, location, null);
}
/** Exception thrown by all basis-template code via {@link ExpressionError#error(String, Span)}. In case an error happens deep inside a
* list of included templates, the {@link #getMessage()} method will return a condensed error message. **/
public static class TemplateException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final Span location;
private final String errorMessage;
private TemplateException (String message, Span location) {
super(message);
this.errorMessage = message;
this.location = location;
}
public TemplateException (String message, Span location, Throwable cause) {
super(message, cause);
this.errorMessage = message;
this.location = location;
}
/** Returns the location in the template at which the error happened. **/
public Span getLocation () {
return location;
}
@Override
public String getMessage () {
StringBuilder builder = new StringBuilder();
if (getCause() == null || getCause() == this) {
return super.getMessage();
}
builder.append(errorMessage.substring(0, errorMessage.indexOf('\n')));
builder.append("\n");
Throwable cause = getCause();
while (cause != null && cause != this) {
if (cause instanceof TemplateException) {
TemplateException ex = (TemplateException)cause;
if (ex.getCause() == null || ex.getCause() == ex)
builder.append(ex.errorMessage);
else
builder.append(ex.errorMessage.substring(0, ex.errorMessage.indexOf('\n')));
builder.append("\n");
}
cause = cause.getCause();
}
return builder.toString();
}
}
public static class StringLiteralException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
}

@ -0,0 +1,34 @@
package org.spiderflow.core.expression;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ExpressionGlobalVariables {
private static Map<String, String> variables = new HashMap<>();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void reset(Map<String, String> map){
Lock lock = readWriteLock.writeLock();
lock.lock();
try {
variables.clear();
variables.putAll(map);
} finally {
lock.unlock();
}
}
public static Map<String, String> getVariables(){
Lock lock = readWriteLock.readLock();
lock.lock();
try {
return variables;
} finally {
lock.unlock();
}
}
}

@ -0,0 +1,38 @@
package org.spiderflow.core.expression;
import java.io.OutputStream;
import java.util.List;
import org.spiderflow.core.expression.interpreter.AstInterpreter;
import org.spiderflow.core.expression.parsing.Ast;
import org.spiderflow.core.expression.parsing.Ast.Node;
import org.spiderflow.core.expression.parsing.Parser;
/** A template is loaded by a {@link TemplateLoader} from a file marked up with the basis-template language. The template can be
* rendered to a {@link String} or {@link OutputStream} by calling one of the <code>render()</code> methods. The
* {@link ExpressionTemplateContext} passed to the <code>render()</code> methods is used to look up variable values referenced in the
* template. */
public class ExpressionTemplate {
private final List<Node> nodes;
/** Internal. Created by {@link Parser}. **/
private ExpressionTemplate (List<Node> nodes) {
this.nodes = nodes;
}
public static ExpressionTemplate create(String source){
return new ExpressionTemplate(Parser.parse(source));
}
/** Internal. The AST nodes representing this template after parsing. See {@link Ast}. Used by {@link AstInterpreter}. **/
public List<Node> getNodes () {
return nodes;
}
/** Renders the template using the TemplateContext to resolve variable values referenced in the template. **/
public Object render (ExpressionTemplateContext context) {
return AstInterpreter.interpret(this, context);
}
}

@ -0,0 +1,112 @@
package org.spiderflow.core.expression;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.spiderflow.core.expression.interpreter.AstInterpreter;
/**
* <p>
* A template context stores mappings from variable names to user provided variable values. A {@link ExpressionTemplate} is given a context
* for rendering to resolve variable values it references in template expressions.
* </p>
*
* <p>
* Internally, a template context is a stack of these mappings, similar to scopes in a programming language, and used as such by
* the {@link AstInterpreter}.
* </p>
*/
public class ExpressionTemplateContext {
private final List<Map<String, Object>> scopes = new ArrayList<Map<String, Object>>();
/** Keeps track of previously allocated, unused scopes. New scopes are first tried to be retrieved from this pool to avoid
* generating garbage. **/
private final List<Map<String, Object>> freeScopes = new ArrayList<Map<String, Object>>();
private final static ThreadLocal<ExpressionTemplateContext> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
public static ExpressionTemplateContext get(){
return CONTEXT_THREAD_LOCAL.get();
}
public static void remove(){
CONTEXT_THREAD_LOCAL.remove();
}
public static void set(ExpressionTemplateContext context){
CONTEXT_THREAD_LOCAL.set(context);
}
public ExpressionTemplateContext () {
push();
}
public ExpressionTemplateContext(Map<String,Object> variables) {
this();
if(variables != null){
variables.forEach(this::set);
}
}
/** Sets the value of the variable with the given name. If the variable already exists in one of the scopes, that variable is
* set. Otherwise the variable is set on the last pushed scope. */
public ExpressionTemplateContext set (String name, Object value) {
for (int i = scopes.size() - 1; i >= 0; i--) {
Map<String, Object> ctx = scopes.get(i);
if (ctx.isEmpty()) continue;
if (ctx.containsKey(name)) {
ctx.put(name, value);
return this;
}
}
scopes.get(scopes.size() - 1).put(name, value);
return this;
}
/** Sets the value of the variable with the given name on the last pushed scope **/
public ExpressionTemplateContext setOnCurrentScope (String name, Object value) {
scopes.get(scopes.size() - 1).put(name, value);
return this;
}
/** Internal. Returns the value of the variable with the given name, walking the scope stack from top to bottom, similar to how
* scopes in programming languages are searched for variables. */
public Object get (String name) {
for (int i = scopes.size() - 1; i >= 0; i--) {
Map<String, Object> ctx = scopes.get(i);
if (ctx.isEmpty()) continue;
Object value = ctx.get(name);
if (value != null) return value;
}
return null;
}
/** Internal. Returns all variables currently defined in this context. */
public Set<String> getVariables () {
Set<String> variables = new HashSet<String>();
for (int i = 0, n = scopes.size(); i < n; i++) {
variables.addAll(scopes.get(i).keySet());
}
return variables;
}
/** Internal. Pushes a new "scope" onto the stack. **/
public void push () {
Map<String, Object> newScope = freeScopes.size() > 0 ? freeScopes.remove(freeScopes.size() - 1) : new HashMap<String, Object>();
scopes.add(newScope);
}
/** Internal. Pops the top of the "scope" stack. **/
public void pop () {
Map<String, Object> oldScope = scopes.remove(scopes.size() - 1);
oldScope.clear();
freeScopes.add(oldScope);
}
}

@ -0,0 +1,67 @@
package org.spiderflow.core.expression.interpreter;
import java.io.IOException;
import java.util.List;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionError.TemplateException;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.ExpressionTemplateContext;
import org.spiderflow.core.expression.parsing.Ast;
import org.spiderflow.core.expression.parsing.Ast.Node;
import org.spiderflow.core.expression.parsing.Ast.Text;
/**
* <p>
* Interprets a Template given a TemplateContext to lookup variable values in and writes the evaluation results to an output
* stream. Uses the global {@link Reflection} instance as returned by {@link Reflection#getInstance()} to access members and call
* methods.
* </p>
*
* <p>
* The interpeter traverses the AST as stored in {@link ExpressionTemplate#getNodes()}. the interpeter has a method for each AST node type
* (see {@link Ast} that evaluates that node. A node may return a value, to be used in the interpretation of a parent node or to
* be written to the output stream.
* </p>
**/
public class AstInterpreter {
public static Object interpret (ExpressionTemplate template, ExpressionTemplateContext context) {
try {
return interpretNodeList(template.getNodes(), template, context);
} catch (Throwable t) {
if (t instanceof TemplateException)
throw (TemplateException)t;
else {
ExpressionError.error("执行表达式出错 " + t.getMessage(), template.getNodes().get(0).getSpan(),t);
return null; // never reached
}
}
}
public static Object interpretNodeList (List<Node> nodes, ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
String result = "";
for (int i = 0, n = nodes.size(); i < n; i++) {
Node node = nodes.get(i);
Object value = node.evaluate(template, context);
if(node instanceof Text){
result += node.getSpan().getText();
}else if(value == null){
if(i == 0 && i + 1 == n){
return null;
}
result += "null";
}else if(value instanceof String || value instanceof Number || value instanceof Boolean){
if(i ==0 && i + 1 ==n){
return value;
}
result += value;
}else if(i + 1 < n){
ExpressionError.error("表达式执行错误", node.getSpan());
}else{
return value;
}
}
return result;
}
}

@ -0,0 +1,365 @@
package org.spiderflow.core.expression.interpreter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class JavaReflection extends Reflection {
private final Map<Class<?>, Map<String, Field>> fieldCache = new ConcurrentHashMap<Class<?>, Map<String, Field>>();
private final Map<Class<?>, Map<JavaReflection.MethodSignature, Method>> methodCache = new ConcurrentHashMap<Class<?>, Map<JavaReflection.MethodSignature, Method>>();
private final Map<Class<?>, Map<String,List<Method>>> extensionmethodCache = new ConcurrentHashMap<>();
@SuppressWarnings("rawtypes")
@Override
public Object getField (Object obj, String name) {
Class cls = obj instanceof Class ? (Class)obj : obj.getClass();
Map<String, Field> fields = fieldCache.get(cls);
if (fields == null) {
fields = new ConcurrentHashMap<String, Field>();
fieldCache.put(cls, fields);
}
Field field = fields.get(name);
if (field == null) {
try {
field = cls.getDeclaredField(name);
field.setAccessible(true);
fields.put(name, field);
} catch (Throwable t) {
// fall through, try super classes
}
if (field == null) {
Class parentClass = cls.getSuperclass();
while (parentClass != Object.class && parentClass != null) {
try {
field = parentClass.getDeclaredField(name);
field.setAccessible(true);
fields.put(name, field);
} catch (NoSuchFieldException e) {
// fall through
}
parentClass = parentClass.getSuperclass();
}
}
}
return field;
}
@Override
public Object getFieldValue (Object obj, Object field) {
Field javaField = (Field)field;
try {
return javaField.get(obj);
} catch (Throwable e) {
throw new RuntimeException("Couldn't get value of field '" + javaField.getName() + "' from object of type '" + obj.getClass().getSimpleName() + "'");
}
}
@Override
public void registerExtensionClass(Class<?> target,Class<?> clazz){
Method[] methods = clazz.getDeclaredMethods();
if(methods != null){
Map<String, List<Method>> cachedMethodMap = extensionmethodCache.get(target);
if(cachedMethodMap == null){
cachedMethodMap = new HashMap<>();
extensionmethodCache.put(target,cachedMethodMap);
}
for (Method method : methods) {
if(Modifier.isStatic(method.getModifiers()) && method.getParameterCount() > 0){
List<Method> cachedList = cachedMethodMap.get(method.getName());
if(cachedList == null){
cachedList = new ArrayList<>();
cachedMethodMap.put(method.getName(), cachedList);
}
cachedList.add(method);
}
}
}
}
@Override
public Object getExtensionMethod(Object obj, String name, Object... arguments) {
Class<?> cls = obj instanceof Class ? (Class<?>)obj : obj.getClass();
if(cls.isArray()){
cls = Object[].class;
}
return getExtensionMethod(cls,name,arguments);
}
private Object getExtensionMethod(Class<?> cls, String name, Object... arguments) {
if(cls == null){
cls = Object.class;
}
Map<String, List<Method>> methodMap = extensionmethodCache.get(cls);
if(methodMap != null){
List<Method> methodList = methodMap.get(name);
if(methodList != null){
Class<?>[] parameterTypes = new Class[arguments.length + 1];
parameterTypes[0] = cls;
for (int i = 0; i < arguments.length; i++) {
parameterTypes[i + 1] = arguments[i] == null ? null : arguments[i].getClass();
}
return findMethod(methodList, parameterTypes);
}
}
if(cls != Object.class){
Class<?>[] interfaces = cls.getInterfaces();
if(interfaces != null){
for (Class<?> clazz : interfaces) {
Object method = getExtensionMethod(clazz,name,arguments);
if(method != null){
return method;
}
}
}
return getExtensionMethod(cls.getSuperclass(),name,arguments);
}
return null;
}
@Override
public Object getMethod (Object obj, String name, Object... arguments) {
Class<?> cls = obj instanceof Class ? (Class<?>)obj : obj.getClass();
Map<JavaReflection.MethodSignature, Method> methods = methodCache.get(cls);
if (methods == null) {
methods = new ConcurrentHashMap<JavaReflection.MethodSignature, Method>();
methodCache.put(cls, methods);
}
Class<?>[] parameterTypes = new Class[arguments.length];
for (int i = 0; i < arguments.length; i++) {
parameterTypes[i] = arguments[i] == null ? null : arguments[i].getClass();
}
JavaReflection.MethodSignature signature = new MethodSignature(name, parameterTypes);
Method method = methods.get(signature);
if (method == null) {
try {
if (name == null) {
method = findApply(cls);
} else {
method = findMethod(cls, name, parameterTypes);
if(method == null && parameterTypes != null){
method = findMethod(cls, name, new Class<?>[]{Object[].class});
}
}
method.setAccessible(true);
methods.put(signature, method);
} catch (Throwable e) {
// fall through
}
if (method == null) {
Class<?> parentClass = cls.getSuperclass();
while (parentClass != Object.class && parentClass != null) {
try {
if (name == null)
method = findApply(parentClass);
else {
method = findMethod(parentClass, name, parameterTypes);
}
method.setAccessible(true);
methods.put(signature, method);
} catch (Throwable e) {
// fall through
}
parentClass = parentClass.getSuperclass();
}
}
}
return method;
}
/** Returns the <code>apply()</code> method of a functional interface. **/
private static Method findApply (Class<?> cls) {
for (Method method : cls.getDeclaredMethods()) {
if (method.getName().equals("apply")) return method;
}
return null;
}
private static Method findMethod (List<Method> methods, Class<?>[] parameterTypes) {
Method foundMethod = null;
int foundScore = 0;
for (Method method : methods) {
// Check if the types match.
Class<?>[] otherTypes = method.getParameterTypes();
if(parameterTypes.length != otherTypes.length){
continue;
}
boolean match = true;
int score = 0;
for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) {
Class<?> type = parameterTypes[ii];
Class<?> otherType = otherTypes[ii];
if (!otherType.isAssignableFrom(type)) {
score++;
if (!isPrimitiveAssignableFrom(type, otherType)) {
score++;
if (!isCoercible(type, otherType)) {
match = false;
break;
} else {
score++;
}
}
}else if(type == null && otherType.isPrimitive()){
match = false;
break;
}
}
if (match) {
if (foundMethod == null) {
foundMethod = method;
foundScore = score;
} else {
if (score < foundScore) {
foundScore = score;
foundMethod = method;
}
}
}
}
return foundMethod;
}
/** Returns the method best matching the given signature, including type coercion, or null. **/
private static Method findMethod (Class<?> cls, String name, Class<?>[] parameterTypes) {
Method[] methods = cls.getDeclaredMethods();
List<Method> methodList = new ArrayList<>();
for (int i = 0, n = methods.length; i < n; i++) {
Method method = methods[i];
// if neither name or parameter list size match, bail on this method
if (!method.getName().equals(name)) continue;
if (method.getParameterTypes().length != parameterTypes.length) continue;
methodList.add(method);
}
return findMethod(methodList,parameterTypes);
}
/** Returns whether the from type can be assigned to the to type, assuming either type is a (boxed) primitive type. We can
* relax the type constraint a little, as we'll invoke a method via reflection. That means the from type will always be boxed,
* as the {@link Method#invoke(Object, Object...)} method takes objects. **/
private static boolean isPrimitiveAssignableFrom (Class<?> from, Class<?> to) {
if ((from == Boolean.class || from == boolean.class) && (to == boolean.class || to == Boolean.class)) return true;
if ((from == Integer.class || from == int.class) && (to == int.class || to == Integer.class)) return true;
if ((from == Float.class || from == float.class) && (to == float.class || to == Float.class)) return true;
if ((from == Double.class || from == double.class) && (to == double.class || to == Double.class)) return true;
if ((from == Byte.class || from == byte.class) && (to == byte.class || to == Byte.class)) return true;
if ((from == Short.class || from == short.class) && (to == short.class || to == Short.class)) return true;
if ((from == Long.class || from == long.class) && (to == long.class || to == Long.class)) return true;
if ((from == Character.class || from == char.class) && (to == char.class || to == Character.class)) return true;
return false;
}
public static String[] getStringTypes(Object[] objects){
String[] parameterTypes = new String[objects == null ? 0: objects.length];
if(objects != null){
for(int i=0,len = objects.length;i<len;i++){
Object value = objects[i];
parameterTypes[i] = value == null ? "null" : value.getClass().getSimpleName();
}
}
return parameterTypes;
}
/** Returns whether the from type can be coerced to the to type. The coercion rules follow those of Java. See JLS 5.1.2
* https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html **/
private static boolean isCoercible (Class<?> from, Class<?> to) {
if (from == Integer.class || from == int.class) {
return to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class || to == Long.class;
}
if (from == Float.class || from == float.class) {
return to == double.class || to == Double.class;
}
if (from == Double.class || from == double.class) {
return false;
}
if (from == Character.class || from == char.class) {
return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
|| to == Long.class;
}
if (from == Byte.class || from == byte.class) {
return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
|| to == Long.class || to == short.class || to == Short.class;
}
if (from == Short.class || from == short.class) {
return to == int.class || to == Integer.class || to == float.class || to == Float.class || to == double.class || to == Double.class || to == long.class
|| to == Long.class;
}
if (from == Long.class || from == long.class) {
return to == float.class || to == Float.class || to == double.class || to == Double.class;
}
if(from == int[].class || from == Integer[].class){
return to == Object[].class || to == float[].class || to == Float[].class || to == double[].class || to == Double[].class || to == long[].class || to == Long[].class;
}
return false;
}
@Override
public Object callMethod (Object obj, Object method, Object... arguments) {
Method javaMethod = (Method)method;
try {
return javaMethod.invoke(obj, arguments);
} catch (Throwable t) {
throw new RuntimeException("Couldn't call method '" + javaMethod.getName() + "' with arguments '" + Arrays.toString(arguments)
+ "' on object of type '" + obj.getClass().getSimpleName() + "'.", t);
}
}
private static class MethodSignature {
private final String name;
@SuppressWarnings("rawtypes") private final Class[] parameters;
private final int hashCode;
@SuppressWarnings("rawtypes")
public MethodSignature (String name, Class[] parameters) {
this.name = name;
this.parameters = parameters;
final int prime = 31;
int hash = 1;
hash = prime * hash + ((name == null) ? 0 : name.hashCode());
hash = prime * hash + Arrays.hashCode(parameters);
hashCode = hash;
}
@Override
public int hashCode () {
return hashCode;
}
@Override
public boolean equals (Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
JavaReflection.MethodSignature other = (JavaReflection.MethodSignature)obj;
if (name == null) {
if (other.name != null) return false;
} else if (!name.equals(other.name)) return false;
if (!Arrays.equals(parameters, other.parameters)) return false;
return true;
}
}
}

@ -0,0 +1,39 @@
package org.spiderflow.core.expression.interpreter;
/** Used by {@link AstInterpreter} to access fields and methods of objects. This is a singleton class used by all
* {@link AstInterpreter} instances. Replace the default implementation via {@link #setInstance(Reflection)}. The implementation
* must be thread-safe. */
public abstract class Reflection {
private static Reflection instance = new JavaReflection();
/** Sets the Reflection instance to be used by all Template interpreters **/
public synchronized static void setInstance (Reflection reflection) {
instance = reflection;
}
/** Returns the Reflection instance used to fetch field and call methods **/
public synchronized static Reflection getInstance () {
return instance;
}
/** Returns an opaque handle to a field with the given name or null if the field could not be found **/
public abstract Object getField (Object obj, String name);
/** Returns an opaque handle to the method with the given name best matching the signature implied by the given arguments, or
* null if the method could not be found. If obj is an instance of Class, the matching static method is returned. If the name
* is null and the object is a {@link FunctionalInterface}, the first declared method on the object is returned. **/
public abstract Object getMethod (Object obj, String name, Object... arguments);
public abstract Object getExtensionMethod (Object obj, String name,Object ... arguments);
public abstract void registerExtensionClass(Class<?> target,Class<?> clazz);
/** Returns the value of the field from the object. The field must have been previously retrieved via
* {@link #getField(Object, String)}. **/
public abstract Object getFieldValue (Object obj, Object field);
/** Calls the method on the object with the given arguments. The method must have been previously retrieved via
* {@link #getMethod(Object, String, Object...)}. **/
public abstract Object callMethod (Object obj, Object method, Object... arguments);
}

@ -0,0 +1,129 @@
package org.spiderflow.core.expression.parsing;
import javax.xml.transform.Source;
/** Wraps a the content of a {@link Source} and handles traversing the contained characters. Manages a current {@link Span} via
* the {@link #startSpan()} and {@link #endSpan()} methods. */
public class CharacterStream {
private final String source;
private int index = 0;
private final int end;
private int spanStart = 0;
public CharacterStream (String source) {
this(source, 0, source.length());
}
public CharacterStream (String source, int start, int end) {
if (start > end) throw new IllegalArgumentException("Start must be <= end.");
if (start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
if (start > Math.max(0, source.length() - 1)) throw new IndexOutOfBoundsException("Start outside of string.");
if (end > source.length()) throw new IndexOutOfBoundsException("End outside of string.");
this.source = source;
this.index = start;
this.end = end;
}
/** Returns whether there are more characters in the stream **/
public boolean hasMore () {
return index < end;
}
/** Returns the next character without advancing the stream **/
public char peek () {
if (!hasMore()) throw new RuntimeException("No more characters in stream.");
return source.charAt(index++);
}
/** Returns the next character and advance the stream **/
public char consume () {
if (!hasMore()) throw new RuntimeException("No more characters in stream.");
return source.charAt(index++);
}
/** Matches the given needle with the next characters. Returns true if the needle is matched, false otherwise. If there's a
* match and consume is true, the stream is advanced by the needle's length. */
public boolean match (String needle, boolean consume) {
int needleLength = needle.length();
if(needleLength + index >end){
return false;
}
for (int i = 0, j = index; i < needleLength; i++, j++) {
if (index >= end) return false;
if (needle.charAt(i) != source.charAt(j)) return false;
}
if (consume) index += needleLength;
return true;
}
/** Returns whether the next character is a digit and optionally consumes it. **/
public boolean matchDigit (boolean consume) {
if (index >= end) return false;
char c = source.charAt(index);
if (Character.isDigit(c)) {
if (consume) index++;
return true;
}
return false;
}
/** Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
* {@link Character#isJavaIdentifierStart(char)}. **/
public boolean matchIdentifierStart (boolean consume) {
if (index >= end) return false;
char c = source.charAt(index);
if (Character.isJavaIdentifierStart(c) || c == '@') {
if (consume) index++;
return true;
}
return false;
}
/** Returns whether the next character is the start of an identifier and optionally consumes it. Adheres to
* {@link Character#isJavaIdentifierPart(char)}. **/
public boolean matchIdentifierPart (boolean consume) {
if (index >= end) return false;
char c = source.charAt(index);
if (Character.isJavaIdentifierPart(c)) {
if (consume) index++;
return true;
}
return false;
}
/** Skips any number of successive whitespace characters. **/
public void skipWhiteSpace () {
while (true) {
if (index >= end) return;
char c = source.charAt(index);
if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
index++;
continue;
} else {
break;
}
}
}
/** Start a new Span at the current stream position. Call {@link #endSpan()} to complete the span. **/
public void startSpan () {
spanStart = index;
}
/** Completes the span started with {@link #startSpan()} at the current stream position. **/
public Span endSpan () {
return new Span(source, spanStart, index);
}
public boolean isSpanEmpty () {
return spanStart == this.index;
}
/** Returns the current character position in the stream. **/
public int getPosition () {
return index;
}
}

@ -0,0 +1,242 @@
package org.spiderflow.core.expression.parsing;
import java.util.ArrayList;
import java.util.List;
import javax.xml.transform.Source;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.parsing.Ast.BinaryOperation;
import org.spiderflow.core.expression.parsing.Ast.BooleanLiteral;
import org.spiderflow.core.expression.parsing.Ast.ByteLiteral;
import org.spiderflow.core.expression.parsing.Ast.CharacterLiteral;
import org.spiderflow.core.expression.parsing.Ast.DoubleLiteral;
import org.spiderflow.core.expression.parsing.Ast.Expression;
import org.spiderflow.core.expression.parsing.Ast.FloatLiteral;
import org.spiderflow.core.expression.parsing.Ast.FunctionCall;
import org.spiderflow.core.expression.parsing.Ast.IntegerLiteral;
import org.spiderflow.core.expression.parsing.Ast.ListLiteral;
import org.spiderflow.core.expression.parsing.Ast.LongLiteral;
import org.spiderflow.core.expression.parsing.Ast.MapLiteral;
import org.spiderflow.core.expression.parsing.Ast.MapOrArrayAccess;
import org.spiderflow.core.expression.parsing.Ast.MemberAccess;
import org.spiderflow.core.expression.parsing.Ast.MethodCall;
import org.spiderflow.core.expression.parsing.Ast.Node;
import org.spiderflow.core.expression.parsing.Ast.NullLiteral;
import org.spiderflow.core.expression.parsing.Ast.ShortLiteral;
import org.spiderflow.core.expression.parsing.Ast.StringLiteral;
import org.spiderflow.core.expression.parsing.Ast.TernaryOperation;
import org.spiderflow.core.expression.parsing.Ast.Text;
import org.spiderflow.core.expression.parsing.Ast.UnaryOperation;
import org.spiderflow.core.expression.parsing.Ast.VariableAccess;
/** Parses a {@link Source} into a {@link ExpressionTemplate}. The implementation is a simple recursive descent parser with a lookahead of
* 1. **/
public class Parser {
/** Parses a {@link Source} into a {@link ExpressionTemplate}. **/
public static List<Node> parse (String source) {
List<Node> nodes = new ArrayList<Node>();
TokenStream stream = new TokenStream(new Tokenizer().tokenize(source));
while (stream.hasMore()) {
nodes.add(parseStatement(stream));
}
return nodes;
}
/** Parse a statement, which may either be a text block, if statement, for statement, while statement, macro definition,
* include statement or an expression. **/
private static Node parseStatement (TokenStream tokens) {
Node result = null;
if (tokens.match(TokenType.TextBlock, false))
result = new Text(tokens.consume().getSpan());
else
result = parseExpression(tokens);
// consume semi-colons as statement delimiters
while (tokens.match(";", true))
;
return result;
}
private static Expression parseExpression (TokenStream stream) {
return parseTernaryOperator(stream);
}
private static Expression parseTernaryOperator (TokenStream stream) {
Expression condition = parseBinaryOperator(stream, 0);
if (stream.match(TokenType.Questionmark, true)) {
Expression trueExpression = parseTernaryOperator(stream);
stream.expect(TokenType.Colon);
Expression falseExpression = parseTernaryOperator(stream);
return new TernaryOperation(condition, trueExpression, falseExpression);
} else {
return condition;
}
}
private static final TokenType[][] binaryOperatorPrecedence = new TokenType[][] {new TokenType[] {TokenType.Assignment},
new TokenType[] {TokenType.Or, TokenType.And, TokenType.Xor}, new TokenType[] {TokenType.Equal, TokenType.NotEqual},
new TokenType[] {TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual}, new TokenType[] {TokenType.Plus, TokenType.Minus},
new TokenType[] {TokenType.ForwardSlash, TokenType.Asterisk, TokenType.Percentage}};
private static Expression parseBinaryOperator (TokenStream stream, int level) {
int nextLevel = level + 1;
Expression left = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
TokenType[] operators = binaryOperatorPrecedence[level];
while (stream.hasMore() && stream.match(false, operators)) {
Token operator = stream.consume();
Expression right = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
left = new BinaryOperation(left, operator, right);
}
return left;
}
private static final TokenType[] unaryOperators = new TokenType[] {TokenType.Not, TokenType.Plus, TokenType.Minus};
private static Expression parseUnaryOperator (TokenStream stream) {
if (stream.match(false, unaryOperators)) {
return new UnaryOperation(stream.consume(), parseUnaryOperator(stream));
} else {
if (stream.match(TokenType.LeftParantheses, true)) {
Expression expression = parseExpression(stream);
stream.expect(TokenType.RightParantheses);
return expression;
} else {
return parseAccessOrCallOrLiteral(stream);
}
}
}
private static Expression parseAccessOrCallOrLiteral (TokenStream stream) {
if (stream.match(TokenType.Identifier, false)) {
return parseAccessOrCall(stream,TokenType.Identifier);
} else if (stream.match(TokenType.LeftCurly, false)) {
return parseMapLiteral(stream);
} else if (stream.match(TokenType.LeftBracket, false)) {
return parseListLiteral(stream);
} else if (stream.match(TokenType.StringLiteral, false)) {
if(stream.hasNext()){
if(stream.next().getType() == TokenType.Period){
stream.prev();
return parseAccessOrCall(stream,TokenType.StringLiteral);
}
stream.prev();
}
return new StringLiteral(stream.expect(TokenType.StringLiteral).getSpan());
} else if (stream.match(TokenType.BooleanLiteral, false)) {
return new BooleanLiteral(stream.expect(TokenType.BooleanLiteral).getSpan());
} else if (stream.match(TokenType.DoubleLiteral, false)) {
return new DoubleLiteral(stream.expect(TokenType.DoubleLiteral).getSpan());
} else if (stream.match(TokenType.FloatLiteral, false)) {
return new FloatLiteral(stream.expect(TokenType.FloatLiteral).getSpan());
} else if (stream.match(TokenType.ByteLiteral, false)) {
return new ByteLiteral(stream.expect(TokenType.ByteLiteral).getSpan());
} else if (stream.match(TokenType.ShortLiteral, false)) {
return new ShortLiteral(stream.expect(TokenType.ShortLiteral).getSpan());
} else if (stream.match(TokenType.IntegerLiteral, false)) {
return new IntegerLiteral(stream.expect(TokenType.IntegerLiteral).getSpan());
} else if (stream.match(TokenType.LongLiteral, false)) {
return new LongLiteral(stream.expect(TokenType.LongLiteral).getSpan());
} else if (stream.match(TokenType.CharacterLiteral, false)) {
return new CharacterLiteral(stream.expect(TokenType.CharacterLiteral).getSpan());
} else if (stream.match(TokenType.NullLiteral, false)) {
return new NullLiteral(stream.expect(TokenType.NullLiteral).getSpan());
} else {
ExpressionError.error("Expected a variable, field, map, array, function or method call, or literal.", stream);
return null; // not reached
}
}
private static Expression parseMapLiteral (TokenStream stream) {
Span openCurly = stream.expect(TokenType.LeftCurly).getSpan();
List<Token> keys = new ArrayList<>();
List<Expression> values = new ArrayList<>();
while (stream.hasMore() && !stream.match("}", false)) {
if(stream.match(TokenType.StringLiteral, false)){
keys.add(stream.expect(TokenType.StringLiteral));
}else{
keys.add(stream.expect(TokenType.Identifier));
}
stream.expect(":");
values.add(parseExpression(stream));
if (!stream.match("}", false)) stream.expect(TokenType.Comma);
}
Span closeCurly = stream.expect("}").getSpan();
return new MapLiteral(new Span(openCurly, closeCurly), keys, values);
}
private static Expression parseListLiteral (TokenStream stream) {
Span openBracket = stream.expect(TokenType.LeftBracket).getSpan();
List<Expression> values = new ArrayList<>();
while (stream.hasMore() && !stream.match(TokenType.RightBracket, false)) {
values.add(parseExpression(stream));
if (!stream.match(TokenType.RightBracket, false)) stream.expect(TokenType.Comma);
}
Span closeBracket = stream.expect(TokenType.RightBracket).getSpan();
return new ListLiteral(new Span(openBracket, closeBracket), values);
}
private static Expression parseAccessOrCall (TokenStream stream,TokenType tokenType) {
//Span identifier = stream.expect(TokenType.Identifier);
//Expression result = new VariableAccess(identifier);
Span identifier = stream.expect(tokenType).getSpan();
Expression result = tokenType == TokenType.StringLiteral ? new StringLiteral(identifier) :new VariableAccess(identifier);
while (stream.hasMore() && stream.match(false, TokenType.LeftParantheses, TokenType.LeftBracket, TokenType.Period)) {
// function or method call
if (stream.match(TokenType.LeftParantheses, false)) {
List<Expression> arguments = parseArguments(stream);
Span closingSpan = stream.expect(TokenType.RightParantheses).getSpan();
if (result instanceof VariableAccess || result instanceof MapOrArrayAccess)
result = new FunctionCall(new Span(result.getSpan(), closingSpan), result, arguments);
else if (result instanceof MemberAccess) {
result = new MethodCall(new Span(result.getSpan(), closingSpan), (MemberAccess)result, arguments);
} else {
ExpressionError.error("Expected a variable, field or method.", stream);
}
}
// map or array access
else if (stream.match(TokenType.LeftBracket, true)) {
Expression keyOrIndex = parseExpression(stream);
Span closingSpan = stream.expect(TokenType.RightBracket).getSpan();
result = new MapOrArrayAccess(new Span(result.getSpan(), closingSpan), result, keyOrIndex);
}
// field or method access
else if (stream.match(TokenType.Period, true)) {
identifier = stream.expect(TokenType.Identifier).getSpan();
result = new MemberAccess(result, identifier);
}
}
return result;
}
/** Does not consume the closing parentheses. **/
private static List<Expression> parseArguments (TokenStream stream) {
stream.expect(TokenType.LeftParantheses);
List<Expression> arguments = new ArrayList<Expression>();
while (stream.hasMore() && !stream.match(TokenType.RightParantheses, false)) {
arguments.add(parseExpression(stream));
if (!stream.match(TokenType.RightParantheses, false)) stream.expect(TokenType.Comma);
}
return arguments;
}
}

@ -0,0 +1,145 @@
package org.spiderflow.core.expression.parsing;
/** A span within a source string denoted by start and end index, with the latter being exclusive. */
public class Span {
/** the source string this span refers to **/
private final String source;
/** start index in source string, starting at 0 **/
private int start;
/** end index in source string, exclusive, starting at 0 **/
private int end;
/** Cached String instance to reduce pressure on GC **/
private final String cachedText;
public Span (String source) {
this(source, 0, source.length());
}
public Span (String source, int start, int end) {
if (start > end) throw new IllegalArgumentException("Start must be <= end.");
if (start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
if (start > source.length() - 1)
throw new IndexOutOfBoundsException("Start outside of string.");
if (end >source.length()) throw new IndexOutOfBoundsException("End outside of string.");
this.source = source;
this.start = start;
this.end = end;
this.cachedText = source.substring(start, end);
}
public Span (Span start, Span end) {
if (!start.source.equals(end.source)) throw new IllegalArgumentException("The two spans do not reference the same source.");
if (start.start > end.end) throw new IllegalArgumentException("Start must be <= end.");
if (start.start < 0) throw new IndexOutOfBoundsException("Start must be >= 0.");
if (start.start > start.source.length() - 1) throw new IndexOutOfBoundsException("Start outside of string.");
if (end.end > start.source.length()) throw new IndexOutOfBoundsException("End outside of string.");
this.source = start.source;
this.start = start.start;
this.end = end.end;
this.cachedText = source.substring(this.start, this.end);
}
/** Returns the text referenced by this span **/
public String getText () {
return cachedText;
}
/** Returns the index of the first character of this span. **/
public int getStart () {
return start;
}
/** Returns the index of the last character of this span plus 1. **/
public int getEnd () {
return end;
}
/** Returns the source string this span references. **/
public String getSource () {
return source;
}
@Override
public String toString () {
return "Span [text=" + getText() + ", start=" + start + ", end=" + end + "]";
}
/** Returns the line this span is on. Does not return a correct result for spans across multiple lines. **/
public Line getLine () {
int lineStart = start;
while (true) {
if (lineStart < 0) break;
char c = source.charAt(lineStart);
if (c == '\n') {
lineStart = lineStart + 1;
break;
}
lineStart--;
}
if (lineStart < 0) lineStart = 0;
int lineEnd = end;
while (true) {
if (lineEnd > source.length() - 1) break;
char c = source.charAt(lineEnd);
if (c == '\n') {
break;
}
lineEnd++;
}
int lineNumber = 0;
int idx = lineStart;
while (idx > 0) {
char c = source.charAt(idx);
if (c == '\n') {
lineNumber++;
}
idx--;
}
lineNumber++;
return new Line(source, lineStart, lineEnd, lineNumber);
}
/** A line within a Source **/
public static class Line {
private final String source;
private final int start;
private final int end;
private final int lineNumber;
public Line (String source, int start, int end, int lineNumber) {
this.source = source;
this.start = start;
this.end = end;
this.lineNumber = lineNumber;
}
public String getSource () {
return source;
}
public int getStart () {
return start;
}
public int getEnd () {
return end;
}
public int getLineNumber () {
return lineNumber;
}
public String getText () {
return source.substring(start, end);
}
}
}

@ -0,0 +1,30 @@
package org.spiderflow.core.expression.parsing;
/** A token produced by the {@link Tokenizer}. */
public class Token {
private final TokenType type;
private final Span span;
public Token (TokenType type, Span span) {
this.type = type;
this.span = span;
}
public TokenType getType () {
return type;
}
public Span getSpan () {
return span;
}
public String getText () {
return span.getText();
}
@Override
public String toString () {
return "Token [type=" + type + ", span=" + span + "]";
}
}

@ -0,0 +1,132 @@
package org.spiderflow.core.expression.parsing;
import java.util.List;
import javax.xml.transform.Source;
import org.spiderflow.core.expression.ExpressionError;
/** Iterates over a list of {@link Token} instances, provides methods to match expected tokens and throw errors in case of a
* mismatch. */
public class TokenStream {
private final List<Token> tokens;
private int index;
private final int end;
public TokenStream (List<Token> tokens) {
this.tokens = tokens;
this.index = 0;
this.end = tokens.size();
}
/** Returns whether there are more tokens in the stream. **/
public boolean hasMore () {
return index < end;
}
public boolean hasNext(){
return index + 1 < end;
}
public boolean hasPrev(){
return index > 0;
}
/** Consumes the next token and returns it. **/
public Token consume () {
if (!hasMore()) throw new RuntimeException("Reached the end of the source.");
return tokens.get(index++);
}
public Token next(){
if (!hasMore()) throw new RuntimeException("Reached the end of the source.");
return tokens.get(++index);
}
public Token prev(){
if(index == 0){
throw new RuntimeException("Reached the end of the source.");
}
return tokens.get(--index);
}
/** Checks if the next token has the give type and optionally consumes, or throws an error if the next token did not match the
* type. */
public Token expect (TokenType type) {
boolean result = match(type, true);
if (!result) {
Token token = index < tokens.size() ? tokens.get(index) : null;
Span span = token != null ? token.getSpan() : null;
if (span == null)
ExpressionError.error("Expected '" + type.getError() + "', but reached the end of the source.", this);
else
ExpressionError.error("Expected '" + type.getError() + "', but got '" + token.getText() + "'", span);
return null; // never reached
} else {
return tokens.get(index - 1);
}
}
/** Checks if the next token matches the given text and optionally consumes, or throws an error if the next token did not match
* the text. */
public Token expect (String text) {
boolean result = match(text, true);
if (!result) {
Token token = index < tokens.size() ? tokens.get(index) : null;
Span span = token != null ? token.getSpan() : null;
if (span == null)
ExpressionError.error("Expected '" + text + "', but reached the end of the source.", this);
else
ExpressionError.error("Expected '" + text + "', but got '" + token.getText() + "'", span);
return null; // never reached
} else {
return tokens.get(index - 1);
}
}
/** Matches and optionally consumes the next token in case of a match. Returns whether the token matched. */
public boolean match (TokenType type, boolean consume) {
if (index >= end) return false;
if (tokens.get(index).getType() == type) {
if (consume) index++;
return true;
}
return false;
}
/** Matches and optionally consumes the next token in case of a match. Returns whether the token matched. */
public boolean match (String text, boolean consume) {
if (index >= end) return false;
if (tokens.get(index).getText().equals(text)) {
if (consume) index++;
return true;
}
return false;
}
/** Matches any of the token types and optionally consumes the next token in case of a match. Returns whether the token
* matched. */
public boolean match (boolean consume, TokenType... types) {
for (TokenType type : types) {
if (match(type, consume)) return true;
}
return false;
}
/** Matches any of the token texts and optionally consumes the next token in case of a match. Returns whether the token
* matched. */
public boolean match (boolean consume, String... tokenTexts) {
for (String text : tokenTexts) {
if (match(text, consume)) return true;
}
return false;
}
/** Returns the {@link Source} this stream wraps. */
public String getSource () {
if (tokens.size() == 0) return null;
return tokens.get(0).getSpan().getSource();
}
}

@ -0,0 +1,101 @@
package org.spiderflow.core.expression.parsing;
import java.util.Arrays;
import java.util.Comparator;
/** Enumeration of token types. A token type consists of a representation for error messages, and may optionally specify a literal
* to be used by the {@link CharacterStream} to recognize the token. Token types are sorted by their literal length to easy
* matching of token types with common prefixes, e.g. "<" and "<=". Token types with longer literals are matched first. */
public enum TokenType {
// @off
TextBlock("a text block"),
Period(".", "."),
Comma(",", ","),
Semicolon(";", ";"),
Colon(":", ":"),
Plus("+", "+"),
Minus("-", "-"),
Asterisk("*", "*"),
ForwardSlash("/", "/"),
PostSlash("\\", "\\"),
Percentage("%", "%"),
LeftParantheses("(", ")"),
RightParantheses(")", ")"),
LeftBracket("[", "["),
RightBracket("]", "]"),
LeftCurly("{", "{"),
RightCurly("}"), // special treatment!
Less("<", "<"),
Greater(">", ">"),
LessEqual("<=", "<="),
GreaterEqual(">=", ">="),
Equal("==", "=="),
NotEqual("!=", "!="),
Assignment("=", "="),
And("&&", "&&"),
Or("||", "||"),
Xor("^", "^"),
Not("!", "!"),
Questionmark("?", "?"),
DoubleQuote("\"", "\""),
SingleQuote("'", "'"),
BooleanLiteral("true or false"),
DoubleLiteral("a double floating point number"),
FloatLiteral("a floating point number"),
LongLiteral("a long integer number"),
IntegerLiteral("an integer number"),
ShortLiteral("a short integer number"),
ByteLiteral("a byte integer number"),
CharacterLiteral("a character"),
StringLiteral("a string"),
NullLiteral("null"),
Identifier("an identifier");
// @on
private static TokenType[] values;
static {
// Sort the token types by their literal length. The character stream uses this
// this order to match tokens with the longest length first.
values = TokenType.values();
Arrays.sort(values, new Comparator<TokenType>() {
@Override
public int compare (TokenType o1, TokenType o2) {
if (o1.literal == null && o2.literal == null) return 0;
if (o1.literal == null && o2.literal != null) return 1;
if (o1.literal != null && o2.literal == null) return -1;
return o2.literal.length() - o1.literal.length();
}
});
}
private final String literal;
private final String error;
TokenType (String error) {
this.literal = null;
this.error = error;
}
TokenType (String literal, String error) {
this.literal = literal;
this.error = error;
}
/** The literal to match, may be null. **/
public String getLiteral () {
return literal;
}
/** The error string to use when reporting this token type in an error message. **/
public String getError () {
return error;
}
/** Returns an array of token types, sorted in descending order based on their literal length. This is used by the
* {@link CharacterStream} to match token types with the longest literal first. E.g. "<=" will be matched before "<". **/
public static TokenType[] getSortedValues () {
return values;
}
}

@ -0,0 +1,191 @@
package org.spiderflow.core.expression.parsing;
import java.util.ArrayList;
import java.util.List;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionError.StringLiteralException;
import org.spiderflow.core.expression.ExpressionError.TemplateException;
public class Tokenizer {
/** Tokenizes the source into tokens with a {@link TokenType}. Text blocks not enclosed in {{ }} are returned as a single token
* of type {@link TokenType.TextBlock}. {{ and }} are not returned as individual tokens. See {@link TokenType} for the list of
* tokens this tokenizer understands. */
public List<Token> tokenize (String source) {
List<Token> tokens = new ArrayList<Token>();
if (source.length() == 0) return tokens;
CharacterStream stream = new CharacterStream(source);
stream.startSpan();
RuntimeException re = null;
while (stream.hasMore()) {
if (stream.match("${", false)) {
if (!stream.isSpanEmpty()) tokens.add(new Token(TokenType.TextBlock, stream.endSpan()));
stream.startSpan();
boolean isContinue = false;
do{
while (!stream.match("}", true)) {
if (!stream.hasMore()) ExpressionError.error("Did not find closing }.", stream.endSpan());
stream.consume();
}
try{
tokens.addAll(tokenizeCodeSpan(stream.endSpan()));
isContinue = false;
re = null;
}catch(TemplateException e){
re = e;
if(e.getCause() != null || stream.hasMore()){
isContinue = true;
}
}
}while(isContinue);
if(re != null){
throw re;
}
stream.startSpan();
} else {
stream.consume();
}
}
if (!stream.isSpanEmpty()) tokens.add(new Token(TokenType.TextBlock, stream.endSpan()));
return tokens;
}
private static List<Token> tokenizeCodeSpan (Span span) {
String source = span.getSource();
CharacterStream stream = new CharacterStream(source, span.getStart(), span.getEnd());
List<Token> tokens = new ArrayList<Token>();
// match opening tag and throw it away
if (!stream.match("${", true)) ExpressionError.error("Expected ${", new Span(source, stream.getPosition(), stream.getPosition() + 1));
int leftCount = 0;
int rightCount = 0;
outer:
while (stream.hasMore()) {
// skip whitespace
stream.skipWhiteSpace();
// Number literal, both integers and floats. Number literals may be suffixed by a type identifier.
if (stream.matchDigit(false)) {
TokenType type = TokenType.IntegerLiteral;
stream.startSpan();
while (stream.matchDigit(true))
;
if (stream.match(TokenType.Period.getLiteral(), true)) {
type = TokenType.FloatLiteral;
while (stream.matchDigit(true))
;
}
if (stream.match("b", true) || stream.match("B", true)) {
if (type == TokenType.FloatLiteral) ExpressionError.error("Byte literal can not have a decimal point.", stream.endSpan());
type = TokenType.ByteLiteral;
} else if (stream.match("s", true) || stream.match("S", true)) {
if (type == TokenType.FloatLiteral) ExpressionError.error("Short literal can not have a decimal point.", stream.endSpan());
type = TokenType.ShortLiteral;
} else if (stream.match("l", true) || stream.match("L", true)) {
if (type == TokenType.FloatLiteral) ExpressionError.error("Long literal can not have a decimal point.", stream.endSpan());
type = TokenType.LongLiteral;
} else if (stream.match("f", true) || stream.match("F", true)) {
type = TokenType.FloatLiteral;
} else if (stream.match("d", true) || stream.match("D", true)) {
type = TokenType.DoubleLiteral;
}
Span numberSpan = stream.endSpan();
tokens.add(new Token(type, numberSpan));
continue;
}
// String literal
if (stream.match(TokenType.SingleQuote.getLiteral(), true)) {
stream.startSpan();
boolean matchedEndQuote = false;
while (stream.hasMore()) {
// Note: escape sequences like \n are parsed in StringLiteral
if (stream.match("\\", true)) {
stream.consume();
}
if (stream.match(TokenType.SingleQuote.getLiteral(), true)) {
matchedEndQuote = true;
break;
}
stream.consume();
}
if (!matchedEndQuote) ExpressionError.error("字符串没有结束符\'", stream.endSpan(),new StringLiteralException());
Span stringSpan = stream.endSpan();
stringSpan = new Span(stringSpan.getSource(), stringSpan.getStart() - 1, stringSpan.getEnd());
tokens.add(new Token(TokenType.StringLiteral, stringSpan));
continue;
}
// String literal
if (stream.match(TokenType.DoubleQuote.getLiteral(), true)) {
stream.startSpan();
boolean matchedEndQuote = false;
while (stream.hasMore()) {
// Note: escape sequences like \n are parsed in StringLiteral
if (stream.match("\\", true)) {
stream.consume();
}
if (stream.match(TokenType.DoubleQuote.getLiteral(), true)) {
matchedEndQuote = true;
break;
}
stream.consume();
}
if (!matchedEndQuote) ExpressionError.error("字符串没有结束符\"", stream.endSpan(),new StringLiteralException());
Span stringSpan = stream.endSpan();
stringSpan = new Span(stringSpan.getSource(), stringSpan.getStart() - 1, stringSpan.getEnd());
tokens.add(new Token(TokenType.StringLiteral, stringSpan));
continue;
}
// Identifier, keyword, boolean literal, or null literal
if (stream.matchIdentifierStart(true)) {
stream.startSpan();
while (stream.matchIdentifierPart(true))
;
Span identifierSpan = stream.endSpan();
identifierSpan = new Span(identifierSpan.getSource(), identifierSpan.getStart() - 1, identifierSpan.getEnd());
if (identifierSpan.getText().equals("true") || identifierSpan.getText().equals("false")) {
tokens.add(new Token(TokenType.BooleanLiteral, identifierSpan));
} else if (identifierSpan.getText().equals("null")) {
tokens.add(new Token(TokenType.NullLiteral, identifierSpan));
} else {
tokens.add(new Token(TokenType.Identifier, identifierSpan));
}
continue;
}
// Simple tokens
for (TokenType t : TokenType.getSortedValues()) {
if (t.getLiteral() != null) {
if (stream.match(t.getLiteral(), true)) {
if(t == TokenType.LeftCurly){
leftCount ++;
}
tokens.add(new Token(t, new Span(source, stream.getPosition() - t.getLiteral().length(), stream.getPosition())));
continue outer;
}
}
}
if(leftCount!=rightCount&&stream.match("}", true)){
rightCount++;
tokens.add(new Token(TokenType.RightCurly, new Span(source, stream.getPosition() - 1, stream.getPosition())));
continue outer;
}
// match closing tag
if (stream.match("}", false)) break;
ExpressionError.error("Unknown token", new Span(source, stream.getPosition(), stream.getPosition() + 1));
}
// code spans must end with }
if (!stream.match("}", true)) ExpressionError.error("Expected }", new Span(source, stream.getPosition(), stream.getPosition() + 1));
return tokens;
}
}

@ -0,0 +1,129 @@
package org.spiderflow.core.io;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.jsoup.Connection;
import org.jsoup.Connection.Method;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
/**
*
* @author Administrator
*
*/
public class HttpRequest {
private Connection connection = null;
public static HttpRequest create(){
return new HttpRequest();
}
public HttpRequest url(String url){
this.connection = Jsoup.connect(url);
this.connection.method(Method.GET);
this.connection.timeout(60000);
return this;
}
public HttpRequest headers(Map<String,String> headers){
this.connection.headers(headers);
return this;
}
public HttpRequest header(String key,String value){
this.connection.header(key, value);
return this;
}
public HttpRequest header(String key,Object value){
if(value != null){
this.connection.header(key,value.toString());
}
return this;
}
public HttpRequest cookies(Map<String,String> cookies){
this.connection.cookies(cookies);
return this;
}
public HttpRequest cookie(String name, String value) {
if (value != null) {
this.connection.cookie(name, value);
}
return this;
}
public HttpRequest contentType(String contentType){
this.connection.header("Content-Type", contentType);
return this;
}
public HttpRequest data(String key,String value){
this.connection.data(key, value);
return this;
}
public HttpRequest data(String key,Object value){
if(value != null){
this.connection.data(key, value.toString());
}
return this;
}
public HttpRequest data(String key,String filename,InputStream is){
this.connection.data(key, filename, is);
return this;
}
public HttpRequest data(Object body){
if(body != null){
this.connection.requestBody(body.toString());
}
return this;
}
public HttpRequest data(Map<String,String> data){
this.connection.data(data);
return this;
}
public HttpRequest method(String method){
this.connection.method(Method.valueOf(method));
return this;
}
public HttpRequest followRedirect(boolean followRedirects){
this.connection.followRedirects(followRedirects);
return this;
}
public HttpRequest timeout(int timeout){
this.connection.timeout(timeout);
return this;
}
public HttpRequest proxy(String host,int port){
this.connection.proxy(host, port);
return this;
}
@SuppressWarnings("deprecation")
public HttpRequest validateTLSCertificates(boolean value){
this.connection.validateTLSCertificates(value);
return this;
}
public HttpResponse execute() throws IOException{
this.connection.ignoreContentType(true);
this.connection.ignoreHttpErrors(true);
this.connection.maxBodySize(0);
Response response = connection.execute();
return new HttpResponse(response);
}
}

@ -0,0 +1,104 @@
package org.spiderflow.core.io;
import com.alibaba.fastjson.JSON;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.spiderflow.io.SpiderResponse;
import java.io.InputStream;
import java.util.Map;
/**
*
* @author Administrator
*
*/
public class HttpResponse implements SpiderResponse{
private Response response;
private int statusCode;
private String urlLink;
private String htmlValue;
private String titleName;
private Object jsonValue;
public HttpResponse(Response response){
super();
this.response = response;
this.statusCode = response.statusCode();
this.urlLink = response.url().toExternalForm();
}
@Override
public int getStatusCode(){
return statusCode;
}
@Override
public String getTitle() {
if (titleName == null) {
synchronized (this){
titleName = Jsoup.parse(getHtml()).title();
}
}
return titleName;
}
@Override
public String getHtml(){
if(htmlValue == null){
synchronized (this){
htmlValue = response.body();
}
}
return htmlValue;
}
@Override
public Object getJson(){
if(jsonValue == null){
jsonValue = JSON.parse(getHtml());
}
return jsonValue;
}
@Override
public Map<String,String> getCookies(){
return response.cookies();
}
@Override
public Map<String,String> getHeaders(){
return response.headers();
}
@Override
public byte[] getBytes(){
return response.bodyAsBytes();
}
@Override
public String getContentType(){
return response.contentType();
}
@Override
public void setCharset(String charset) {
this.response.charset(charset);
}
@Override
public String getUrl() {
return urlLink;
}
@Override
public InputStream getStream() {
return response.bodyStream();
}
}

@ -0,0 +1,102 @@
package org.spiderflow.core.job;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.context.SpiderContextHolder;
import org.spiderflow.core.Spider;
import org.spiderflow.core.model.SpiderFlow;
import org.spiderflow.core.model.Task;
import org.spiderflow.core.service.SpiderFlowService;
import org.spiderflow.core.service.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
*
*
* @author Administrator
*/
@Component
public class SpiderJob extends QuartzJobBean {
@Autowired
private Spider spider;
@Autowired
private SpiderFlowService spiderFlowService;
@Autowired
private TaskService taskService;
private static Map<Integer, SpiderContext> contextMap = new HashMap<>();
@Value("${spider.job.enable:true}")
private boolean spiderJobEnable;
@Value("${spider.workspace}")
private String workspace;
private static Logger logger = LoggerFactory.getLogger(SpiderJob.class);
@Override
protected void executeInternal(JobExecutionContext context) {
if (!spiderJobEnable) {
return;
}
JobDataMap dataMap = context.getMergedJobDataMap();
SpiderFlow spiderFlow = (SpiderFlow) dataMap.get(SpiderJobManager.JOB_PARAM_NAME);
if("1".equalsIgnoreCase(spiderFlow.getEnabled())){
run(spiderFlow, context.getNextFireTime());
}
}
public void run(String id) {
run(spiderFlowService.getById(id), null);
}
public void run(SpiderFlow spiderFlow, Date nextExecuteTime) {
Task task = new Task();
task.setFlowId(spiderFlow.getId());
task.setBeginTime(new Date());
taskService.save(task);
run(spiderFlow,task,nextExecuteTime);
}
public void run(SpiderFlow spiderFlow, Task task,Date nextExecuteTime) {
SpiderJobContext context = null;
Date now = new Date();
try {
context = SpiderJobContext.create(this.workspace, spiderFlow.getId(),task.getId(),false);
SpiderContextHolder.set(context);
contextMap.put(task.getId(), context);
logger.info("开始执行任务{}", spiderFlow.getName());
spider.run(spiderFlow, context);
logger.info("执行任务{}完毕,下次执行时间:{}", spiderFlow.getName(), nextExecuteTime == null ? null : DateFormatUtils.format(nextExecuteTime, "yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
logger.error("执行任务{}出错", spiderFlow.getName(), e);
} finally {
if (context != null) {
context.close();
}
task.setEndTime(new Date());
taskService.saveOrUpdate(task);
contextMap.remove(task.getId());
SpiderContextHolder.remove();
}
spiderFlowService.executeCountIncrement(spiderFlow.getId(), now, nextExecuteTime);
}
public static SpiderContext getSpiderContext(Integer taskId) {
return contextMap.get(taskId);
}
}

@ -0,0 +1,73 @@
package org.spiderflow.core.job;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.context.SpiderContext;
import org.spiderflow.model.SpiderOutput;
public class SpiderJobContext extends SpiderContext{
private static final long serialVersionUID = 9099787449108938453L;
private static Logger logger = LoggerFactory.getLogger(SpiderJobContext.class);
private OutputStream outputstream;
private List<SpiderOutput> outputs = new ArrayList<>();
private boolean output;
public SpiderJobContext(OutputStream outputstream,boolean output) {
super();
this.outputstream = outputstream;
this.output = output;
}
public void close(){
try {
this.outputstream.close();
} catch (Exception e) {
}
}
@Override
public void addOutput(SpiderOutput output) {
if(this.output){
synchronized (this.outputs){
this.outputs.add(output);
}
}
}
@Override
public List<SpiderOutput> getOutputs() {
return outputs;
}
public OutputStream getOutputstream(){
return this.outputstream;
}
public static SpiderJobContext create(String directory,String id,Integer taskId,boolean output){
OutputStream os = null;
try {
File file = new File(new File(directory),id + File.separator + "logs" + File.separator + taskId + ".log");
File dirFile = file.getParentFile();
if(!dirFile.exists()){
dirFile.mkdirs();
}
os = new FileOutputStream(file, true);
} catch (Exception e) {
logger.error("创建日志文件出错",e);
}
SpiderJobContext context = new SpiderJobContext(os, output);
context.setFlowId(id);
return context;
}
}

@ -0,0 +1,89 @@
package org.spiderflow.core.job;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spiderflow.core.Spider;
import org.spiderflow.core.model.SpiderFlow;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
*
* @author Administrator
*
*/
@Component
public class SpiderJobManager {
private static Logger logger = LoggerFactory.getLogger(SpiderJobManager.class);
private final static String JOB_NAME = "SPIDER_TASK_";
public final static String JOB_PARAM_NAME = "SPIDER_FLOW";
@Autowired
private SpiderJob spiderJob;
/**
*
*/
@Autowired
private Scheduler scheduler;
private JobKey getJobKey(String id){
return JobKey.jobKey(JOB_NAME + id);
}
private TriggerKey getTriggerKey(String id){
return TriggerKey.triggerKey(JOB_NAME + id);
}
/**
*
* @param spiderFlow
* @return boolean true/false
*/
public Date addJob(SpiderFlow spiderFlow){
try {
JobDetail job = JobBuilder.newJob(SpiderJob.class).withIdentity(getJobKey(spiderFlow.getId())).build();
job.getJobDataMap().put(JOB_PARAM_NAME, spiderFlow);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(spiderFlow.getCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(spiderFlow.getId())).withSchedule(cronScheduleBuilder).build();
return scheduler.scheduleJob(job,trigger);
} catch (SchedulerException e) {
logger.error("创建定时任务出错",e);
return null;
}
}
public void run(String id){
Spider.executorInstance.submit(()->{
spiderJob.run(id);
});
}
public boolean remove(String id){
try {
scheduler.deleteJob(getJobKey(id));
return true;
} catch (SchedulerException e) {
logger.error("删除定时任务失败",e);
return false;
}
}
}

@ -0,0 +1,15 @@
package org.spiderflow.core.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Select;
import org.spiderflow.core.model.DataSource;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface DataSourceMapper extends BaseMapper<DataSource>{
@Select("select id,name from sp_datasource")
List<DataSource> selectAll();
}

@ -0,0 +1,9 @@
package org.spiderflow.core.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.spiderflow.core.model.FlowNotice;
@Mapper
public interface FlowNoticeMapper extends BaseMapper<FlowNotice> {
}

@ -0,0 +1,10 @@
package org.spiderflow.core.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.spiderflow.core.model.Function;
@Mapper
public interface FunctionMapper extends BaseMapper<Function> {
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save