# 功能描述

实现通用的数据增删改查等功能。 导入本包就具有通用查询及保存服务。 前后端通信格式:

提示

为了减少传输次数,可配置在保存同时将最新查询结果返回。

# 数据增删改查使用

# 第一步 引入包

<dependency>
    <groupId>sei-cloud</groupId>
    <artifactId>data</artifactId>
</dependency>
1
2
3
4

# 第二步 引入接口

@Resource
    CommonQuerySaveService commonQuerySaveService;
1
2

# 第三步 使用接口

/**
 * 通用数据查询保存服务
 */
public interface CommonQuerySaveService {

    /**
     * 通用数据保存
     * @param requestSave: JSON内容
	 * @return ResMsg
     * @throws Exception 异常
     */
    ResMsg save(@NonNull JSON requestSave) throws Exception;

	/**
	 * 获得保存实体
	 * @param json: JSON内容
	 * @return SQLSaveEntry
	 * @throws SQLException 异常
	 * @throws InstantiationException 异常
	 * @throws IntrospectionException 异常
	 * @throws IllegalAccessException 异常
	 * @throws IllegalArgumentException 异常
	 * @throws InvocationTargetException 异常
	 */
	DataSaveEntity getDataSaveEntry(JSON json) throws SQLException, InstantiationException, IntrospectionException, IllegalAccessException, IllegalArgumentException, InvocationTargetException;

	/**
	 * 通用数据查询
	 * @param requestSave: JSON内容
	 * @return ResMsg
	 * @throws Exception 异常
	 */
	ResMsg query(@NonNull JSONObject requestSave) throws Exception;

	/**
	 * 获得查询实体
	 * @param json: 前端json
	 * @return SqlQueryEntry
	 * @throws SQLException 异常
	 * @throws IllegalArgumentException 异常
	 */
	DataQueryEntity getQueryEntry(@NonNull JSONObject json) throws SQLException, IllegalArgumentException;

	/**
	 * 校验业务通信的JSON头部
	 * @param head: 头部
	 * @throws SQLException 异常
	 */
	void checkHead(@NonNull JSONObject head) throws SQLException;

	/**
	 * 专用于admin在前端数据源测试编写的SQL查询语句
	 * @param requestQuery: 内容
	 * @return ResMsg
	 */
	ResMsg querySQL(@NonNull JSONObject requestQuery);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# Service接口

# 第一步 引入包

<dependency>
    <groupId>sei-cloud</groupId>
    <artifactId>data</artifactId>
</dependency>
1
2
3
4

# 第二步 引入接口

@Resource
    UserService userService;
1
2

# 第三步 使用接口

/**
 * 用户服务接口
 * @author xiong
 */
public interface UserService {

    /**
     * 获得用户菜单
     * @param from: 从哪个页面发起的请求
     * @return List
     * @throws SQLException 异常
     */
    ResMsg getMenu(String from) throws SQLException;

    /**
     * 设置用户菜单
     * @param json: 内容
     * @return ResMsg
     * @throws SQLException 异常
     * @throws InstantiationException 异常
     * @throws IntrospectionException 异常
     * @throws IllegalAccessException 异常
     * @throws IllegalArgumentException 异常
     * @throws InvocationTargetException 异常
     */
    ResMsg setMenu(JSONObject json) throws SQLException, InstantiationException, IntrospectionException, IllegalAccessException, IllegalArgumentException, InvocationTargetException;

    /**
     * 用户更改口令
     * @param json: 内容
     * @return ResMsg
     * @throws SQLException 异常
     */
    ResMsg changePassword(JSONArray json) throws SQLException;

    /**
     * 用户信息
     * @param uid: 用户登录账号
     * @return ResMsg
     * @throws SQLException 异常
     */
    ResMsg getUserInfo(String uid) throws SQLException;

    /**
     * 保存用户信息
     * @param json: 用户信息
     * @return ResMsg
     * @throws SQLException 异常
     * @throws InstantiationException 异常
     * @throws IntrospectionException 异常
     * @throws IllegalAccessException 异常
     * @throws IllegalArgumentException 异常
     * @throws InvocationTargetException 异常
     * @throws InvalidFormatException 异常
     */
    ResMsg setUserInfo(JSON json) throws Exception;

    /**
     * 获得当前用户或指定用户的上级单位
     * @param json: json数据
     * @return ResMsg
     * @throws SQLException 异常
     */
    ResMsg getParentOrg(JSONObject json) throws SQLException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

# Controller接口

数据查询与保存服务接口:

@Api(description = "数据查询与保存服务")
@RequestMapping(value = "/api/data/")
public class DataBusController {

    @ApiOperation(value = "数据保存服务(公共接口)",response = ResMsg.class)
    @RequestMapping(value = "/public/save",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg publicSave(@RequestBody Object json) throws Exception;

    @ApiOperation(value = "数据查询服务(公共接口)",response = ResMsg.class)
    @RequestMapping(value = "/public/query",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg publicQuery(@RequestBody JSONObject json) throws Exception;

    @ApiOperation(value = "SQL编辑器查询服务",response = ResMsg.class)
    @RequestMapping(value = "/querySQL",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg querySQL(@RequestBody JSONObject json);

    @ApiOperation(value = "清除所有缓存服务",response = ResMsg.class)
    @RequestMapping(value = "/removeAllCache",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg removeAllCache();

    @ApiOperation(value = "获得所有表名称服务",response = ResMsg.class)
    @RequestMapping(value = "/getAllTablesName",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getAllTablesName() throws SQLException;

    @ApiOperation(value = "获得指定表中所有字段名服务",response = ResMsg.class)
    @RequestMapping(value = "/getTableAllFieldsName",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getTableAllFieldsName(@RequestBody JSONObject json) throws Exception;

    @ApiOperation(value = "获得所有表名及所有字段名服务",response = ResMsg.class)
    @RequestMapping(value = "/getAllTableAllFieldsName",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getAllTableAllFieldsName() throws SQLException;

    @ApiOperation(value = "获得指定模块的表、视图和自定义SQL",response = ResMsg.class)
    @RequestMapping(value = "/getModuleInfo",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getModuleInfo(@RequestBody JSONObject json) throws SQLException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

用户服务接口:

@Api(description = "用户服务")
@RequestMapping(value = "/api/user/")
public class UserController {

    @ApiOperation(value = "获得用户菜单",response = ResMsg.class)
    @RequestMapping(value = "/public/getMenu",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getMenu(@RequestBody JSONObject json) throws Exception;

    @ApiOperation(value = "获得用户信息",response = ResMsg.class)
    @RequestMapping(value = "/public/getUserInfo",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg getUserInfo(@RequestBody JSONObject json) throws Exception;

    @ApiOperation(value = "保存用户信息",response = ResMsg.class)
    @RequestMapping(value = "/public/saveUserInfo",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg saveUserInfo(@RequestBody Object json) throws Exception;

    @ApiOperation(value = "设置用户菜单",response = ResMsg.class)
    @RequestMapping(value = "/setMenu",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg setMenu(@RequestBody JSONObject json) throws Exception;

    @ApiOperation(value = "更改口令",response = ResMsg.class)
    @RequestMapping(value = "/changePassword",method = {RequestMethod.POST},produces = { MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody ResMsg changePassword(@RequestBody JSONArray json) throws Exception;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 扩展事件

# 用户菜单定制化

  1. 抽象类定义
public abstract class UserExtEvent {

    /**
     * 设置菜单业务扩展信息
     * @param resMsg: 返回前端的数据(已经包含了sessionUser)
     */
    public void userMenuExtInfo(ResMsg resMsg) {

    }

}
1
2
3
4
5
6
7
8
9
10
11
  1. 使用方式 新建一个自己的类并集成UserExtEvent类,覆盖userMenuExtInfo方法。 例如:
public class MyUserExtEvent extends UserExtEvent {

    /**
     * 设置菜单业务扩展信息
     * @param resMsg: 返回前端的数据(已经包含了sessionUser)
     */
    public void userMenuExtInfo(ResMsg resMsg) {
        //TODO 修改 resMsg 对象内的值
    }

}
1
2
3
4
5
6
7
8
9
10
11

# 数据解析和增/删/改/查的定制化

本组件在 增/删/改/查 时定义了不同的事件,用户可通过继承不同的类并覆盖默认方法达到定制化效果。

  • DataParseEvent 抽象类实现对前端传回的JSON格式解析转换为SQL实体前后的定制化
  • DataPrivilegeEvent 抽象类实现自定义权限
  • DataQueryEvent 抽象类实现对查询定制化,包括将SQL实体转换为SQL语句以及执行前后的定制化
  • DataSaveEvent 抽象类实现对数据增/删/改时的定制化

# JSON解析转换事件

/**
 * 接收到前端JSON请求并将它转换为SQL实体前后的事件
 */
public abstract class DataParseEvent {
	private int order;	/* 多个本类时的排序 */

	/**
	 * 将前端JSON解析为SQL保存实体前调用
	 * @param json: 前端请求的JSON
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则继续后续操作,否则终止并将 resMsg 信息返回前端
	 */
	public boolean beforeParseSave(JSON json, ResMsg resMsg) {
		return true;
	}

	/**
	 * 将前端JSON解析为SQL保存实体后调用
	 * @param json: 前端请求的JSON
	 * @param sqlSaveEntry: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则继续后续操作,否则终止并将 resMsg 信息返回前端
	 */
	public boolean afterParseSave(JSON json, DataSaveEntity sqlSaveEntry, ResMsg resMsg) {
		return true;
	}

	/**
	 * 将前端JSON解析为SQL查询实体前调用
	 * @param json: 前端请求的JSON
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则继续后续操作,否则终止并将 resMsg 信息返回前端
	 */
	public boolean beforeParseQuery(JSONObject json, ResMsg resMsg) {
		return true;
	}

	/**
	 * 将前端JSON解析为SQL查询实体前调用
	 * @param json: 前端请求的JSON
	 * @param resMsg: 终止时返回前端的结果
	 * @param sqlQueryEntry: SQL查询实体
	 * @return 返回true则继续后续操作,否则终止并将 resMsg 信息返回前端
	 */
	public boolean afterParseQuery(JSONObject json, DataQueryEntity sqlQueryEntry, ResMsg resMsg) {
		return true;
	}

	/**
	 * 获得当前类的排序
	 * @return int
	 */
	public int getOrder() {
		return order;
	}

	/**
	 * 设置当前类的排序
	 * @param order: 排序
	 */
	public void setOrder(int order) {
		this.order = order;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# 权限检查事件

/**
 * 权限检查事件
 * 提示:
 * 		通过 PrivilegeServiceImpl.getInstance() 获得系统权限操作服务
 * 		通过 SQLServiceImpl.getInstance() 获得系统SQL操作服务
 */
public abstract class DataPrivilegeEvent {
	private int order;	/* 多个本类时的排序 */

	/**
	 * 检查权限
	 * @param sqlvo: SQL对象
	 * @return 返回true执行后续检查,false终止操作并抛出无权限提示
	 */
	public abstract boolean checkQueryPrivilege(SQLVo sqlvo);

	/**
	 * 检查权限(覆盖默认的权限检查)
	 * @param sqlEntry: SQLEntry实体
	 * @return PrivilegeVo
	 */
	public abstract PrivilegeVo getPrivilege(DataEntity sqlEntry);

	/**
	 * 获得当前类的排序
	 * @return int
	 */
	public int getOrder() {
		return order;
	}

	/**
	 * 设置当前类的排序
	 * @param order: 排序
	 */
	public void setOrder(int order) {
		this.order = order;
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 查询转换及执行前后事件

/**
 * 查询转换及执行前后事件
 * 提示: 通过 SqlServiceImpl.getInstance() 获得数据操作服务
 */
public abstract class DataQueryEvent {
	private int order;	/* 多个本类时的排序 */

	/**
	 * SQL查询实体转换为SQL语句前调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param sqlQueryEntry: SQL查询实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则进行转换,否则终止
	 */
	public boolean beforeConvert(DataQueryEntity sqlQueryEntry, ResMsg resMsg) {
		return true;
	}

	/**
	 * SQL查询实体转换为SQL语句后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param sqlQueryEntry: SQL查询实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true进行下一步查询操作,否则终止
	 */
	public boolean afterConvert(DataQueryEntity sqlQueryEntry, ResMsg resMsg) {
		return true;
	}

	/**
	 * 查询前调用,与tableName值有关,只有前端配置为table或views并且操作与tableName值相等才会调用(前端配置为source或sql时无效)
	 * @param sqlQueryEntry: SQL查询实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则执行查询,否则终止查询
	 */
	public boolean beforeQuery(DataQueryEntity sqlQueryEntry, ResMsg resMsg) {
		return true;
	}

	/**
	 * 查询后调用(可通过sqlQueryEntry.getResMsg()获得查询结果),与tableName值有关,只有前端配置为table或views并且操作与tableName值相等才会调用(前端配置为source或sql时无效)
	 * @param resMsg: 终止时返回前端的结果
	 * @param sqlQueryEntry: SQL查询实体
	 */
	public void afterQuery(DataQueryEntity sqlQueryEntry, ResMsg resMsg) {

	}

	/**
	 * 获得当前类的排序
	 * @return int
	 */
	public int getOrder() {
		return order;
	}

	/**
	 * 设置当前类的排序
	 * @param order: 排序
	 */
	public void setOrder(int order) {
		this.order = order;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 保存转换及执行前后事件

/**
 * 保存转换及执行前后事件
 * 提示: 通过 SqlServiceImpl.getInstance() 获得数据操作服务
 */
public abstract class DataSaveEvent {
	private String tableName;	/* 表名,多个表名使用逗号分隔 */
	private int order;	/* 多个本类时的排序 */

	/**
	 * 注册表名。
	 * 例如,注册对yw_demo表操作时执行本类:
	 * public void registerTable(){
	 *     this.setTableAndOrder("yw_demo", 0);
	 * }
	 */
	public abstract void registerTable();

	/**
	 * 设置挂载的表及调用顺序
	 * @param tableName: 挂载的表(例如挂载到sys_user表,则只有对sys_user表操作时才调用本类)。
	 * @param order: 调用顺序
	 */
	public void setTableAndOrder(@NonNull String tableName, int order) {
		this.tableName = tableName;
		this.order = order;
	}

	/**
	 * SQL保存实体转换为SQL语句前调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则进行转换,否则终止
	 */
	public boolean beforeConvert(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * SQL保存实体转换为SQL语句后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则进行下一步保存操作,否则终止
	 */
	public boolean afterConvert(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * 数据保存前调用(包括INSERT, UPDATE, DELETE时都会调用),与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则执行保存操作,否则终止
	 */
	public boolean beforeSave(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * 数据保存后调用(包括INSERT, UPDATE, DELETE时都会调用),与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 */
	public void afterSave(DataSaveEntity dataSaveEntity, ResMsg resMsg) {

	}

	/**
	 * 数据插入(INSERT)前调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则执行插入,否则终止
	 */
	public boolean beforeInsert(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * 数据插入(INSERT)后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 */
	public void afterInsert(DataSaveEntity dataSaveEntity, ResMsg resMsg) {

	}

	/**
	 * 数据修改(UPDATE)前调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则执行修改,否则终止
	 */
	public boolean beforeUpdate(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * 数据修改(UPDATE)后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 */
	public void afterUpdate(DataSaveEntity dataSaveEntity, ResMsg resMsg) {

	}

	/**
	 * 数据删除(DELETE)前调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 * @return 返回true则执行删除,否则终止
	 */
	public boolean beforeDelete(DataSaveEntity dataSaveEntity, ResMsg resMsg) {
		return true;
	}

	/**
	 * 数据删除(DELETE)后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param dataSaveEntity: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 */
	public void afterDelete(DataSaveEntity dataSaveEntity, ResMsg resMsg) {

	}

	/**
	 * 获得当前操作表名称
	 * @return String
	 */
	public String getTableName() {
		return tableName;
	}

	/**
	 * 设置当前操作表名称
	 * @param tableName: 表名称
	 */
	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	/**
	 * 获得当前类的排序
	 * @return int
	 */
	public int getOrder() {
		return order;
	}

	/**
	 * 设置当前类的排序
	 * @param order: 排序
	 */
	public void setOrder(int order) {
		this.order = order;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

# 例1: 插入后事件

例如针对yw_demo进行插入新数据有干其它事情。

public class Test extends DataEvent {
	@Override
	public void registerTable() {
		this.setTableAndOrder("yw_demo", 0);
	}

	/**
	 * 数据插入(INSERT)后调用,与tableName值有关,只有操作的表与tableName值相等才会调用
	 * @param sqlSaveEntry: SQL保存实体
	 * @param resMsg: 终止时返回前端的结果
	 */
    @Override
	public void afterInsert(DataSaveEntity sqlSaveEntry, ResMsg resMsg) {
		//TODO 需要干的事情
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 例2: 更改查询前更改查询条件

public class Test extends DataEvent {
	@Override
	public void registerTable() {
		this.setTableAndOrder(null, 0);
	}

    /**
	 * 查询前调用,与tableName值有关,只有前端配置为table或views并且操作与tableName值相等才会调用(前端配置为source或sql时无效)
	 * @param sqlQueryEntry: SQL查询实体
	 */
	public boolean beforeSelect(SQLQueryEntry sqlQueryEntry) {
		//TODO 更改 SQLQueryEntry 实体内容

		//方式1:设置sqlQueryEntry实体的SQL为null,则执行查询时会自动转换
		sqlQueryEntry.setDoSQL(null);

		//方式2:如果不使用上步则可使用下句手动调用转换语句重新生成SQL
		try {
			super.getCommonQuerySaveService().convert2SQL(sqlQueryEntry);
		}catch (Exception e) {
			Constants.logger.error(e.getMessage(), e);
		}
		return true;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25