package com.uas.kanban.support; import com.uas.kanban.annotation.NotEmpty; import com.uas.kanban.model.GlobalParameter; import com.uas.kanban.model.Type; import com.uas.kanban.util.ArrayUtils; import com.uas.kanban.util.CollectionUtils; import com.uas.kanban.util.StringUtils; import me.chyxion.jdbc.NewbieJdbcSupport; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.dom4j.tree.DefaultElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.xml.transform.TransformerException; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author sunyj * @since 2017年9月7日 下午2:52:04 */ @Component public class TemplateParser { /** * 查询条件的结果数目限制 */ private static final int MAX_RECORD_SIZE = 10000; private Logger logger = LoggerFactory.getLogger(getClass()); /** * 替换模版中的参数为实际值 * * @param content * 模版内容 * @param globalParameters * 公共参数 * @param needSingleQuote * 参数值是否以单引号括起来(用于 sql) * @param jdbc * NewbieJdbcSupport 对象 * @return 替换后的模版内容 * @throws SQLException */ private String replaceParameters(@NotEmpty("content") String content, List globalParameters, @NotEmpty("needSingleQuote") Boolean needSingleQuote, @NotEmpty("jdbc") NewbieJdbcSupport jdbc) throws SQLException { if (CollectionUtils.isEmpty(globalParameters)) { return content; } Map map = new HashMap<>(); for (GlobalParameter globalParameter : globalParameters) { map.put(globalParameter.getName(), globalParameter); } String regex = "\\{([\\s\\S]+?)\\}"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(content); while (matcher.find()) { int start = matcher.start(); int end = matcher.end(); String globalParameterName = matcher.group(1); GlobalParameter globalParameter = map.get(globalParameterName); if (globalParameter == null) { throw new IllegalStateException("未启用公共参数:" + globalParameterName); } Object value = globalParameter.getValue(); if (value == null) { throw new IllegalStateException("公共参数值为null:" + globalParameterName); } // 如果公共参数值为 {@link Type#SQL} ,需要取得查询结果 if(globalParameter.getType()== Type.SQL){ String stringValue=(String)value; // 不递归替换公共参数 // stringValue=replaceParameters(stringValue,globalParameters,true,jdbc); checkCount(jdbc.getDataSource().getConnection(), stringValue); Map data = jdbc.findMap(stringValue); if (CollectionUtils.isEmpty(data)) { throw new IllegalStateException("公共参数通过 SQL 取得结果为空:"+globalParameterName); }else if(data.size()>1){ throw new IllegalStateException("公共参数通过 SQL 取得结果不止一列:"+globalParameterName); } value=data.values().iterator().next(); } // TODO value 为时间 if (needSingleQuote != null && needSingleQuote.booleanValue()) { content = content.substring(0, start) + "'" + value + "'" + content.substring(end); } else { content = content.substring(0, start) + value + content.substring(end); } matcher = pattern.matcher(content); } return content; } /** * 解析单个 xml 模版,转为 json 格式的数据 * * @param content * 模版内容 * @param title * 标题 * @param globalParameters * 公共参数 * @param jdbc * NewbieJdbcSupport 对象 * @return 解析后的 json 数据 * @throws DocumentException * @throws IOException * @throws TransformerException * @throws SQLException * @throws IllegalStateException */ public String parseXml(@NotEmpty("content") String content, String title,List globalParameters, @NotEmpty("jdbc") NewbieJdbcSupport jdbc) throws DocumentException, TransformerException, IOException, IllegalStateException, SQLException { content = processSql(content); // 替换标题中的公共参数 if (!com.uas.kanban.util.StringUtils.isEmpty(title)) { title = replaceParameters(title, globalParameters, false,jdbc); } content = processForm(content, jdbc, globalParameters); content = processGrid(content, jdbc, globalParameters); content = processBarAndLine(content, jdbc, globalParameters); content = processPie(content, jdbc, globalParameters); content = finalProcess(content, title); return TranslateHelper.xmlToJson(content); } /** * 处理 sql ,包括替换特殊字符等 * * @param content * 模版内容 * @return 处理后的模版内容 */ public String processSql(@NotEmpty("content") String content) { String regex = "sql=\"([^\"]+?[<>]+?[^\"]+?)\""; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(content); while (matcher.find()) { String sql = matcher.group(1); int start = matcher.start(1); int end = matcher.end(1); sql = sql.replaceAll(">", ">").replaceAll("<", "<"); content = content.substring(0, start) + sql + content.substring(end); matcher = pattern.matcher(content); } return content; } /** * 处理模版中的 form 组件 * * @param content * 模版内容 * @param jdbc * NewbieJdbcSupport 对象 * @param globalParameters * 公共参数 * @return 解析后的模版内容 * @throws DocumentException * @throws SQLException * @throws IllegalStateException */ @SuppressWarnings("unchecked") private String processForm(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List globalParameters) throws DocumentException, IllegalStateException, SQLException { Document document = getDocument(content); // 获取 form 组件 List elements = document.selectNodes("//form"); for (Element element : elements) { // 执行 sql ,获取数据 String sql = element.attribute("sql").getText(); if(StringUtils.isEmpty(sql)){ continue; } sql = replaceParameters(sql, globalParameters, true,jdbc); // 获取 field 节点 List fieldElements = element.elements("field"); if (CollectionUtils.isEmpty(fieldElements)) { continue; } checkCount(jdbc.getDataSource().getConnection(), sql); Map map = jdbc.findMap(sql); if (CollectionUtils.isEmpty(map)) { continue; } for (Element fieldElement : fieldElements) { // 设置 field 的 value 属性 Attribute valueAttribute = fieldElement.attribute("value"); String fieldName = valueAttribute.getText(); valueAttribute.setText(toString(map.get(fieldName.toUpperCase()))); } } return document.asXML(); } /** * 处理模版中的 grid 组件 * * @param content * 模版内容 * @param jdbc * NewbieJdbcSupport 对象 * @param globalParameters * 公共参数 * @return 解析后的模版内容 * @throws DocumentException * @throws SQLException * @throws IllegalStateException */ @SuppressWarnings("unchecked") private String processGrid(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List globalParameters) throws DocumentException, IllegalStateException, SQLException { Document document = getDocument(content); // 获取 form 组件 List elements = document.selectNodes("//grid"); for (Element element : elements) { // 执行 sql ,获取数据 String sql = element.attribute("sql").getText(); if(StringUtils.isEmpty(sql)){ continue; } sql = replaceParameters(sql, globalParameters, true,jdbc); // 获取 field 节点 List fieldElements = element.elements("field"); if (CollectionUtils.isEmpty(fieldElements)) { continue; } // 提取 field 名称 List fieldNames = new ArrayList<>(); for (Element fieldElement : fieldElements) { // 设置 field 的 name String fieldName = fieldElement.attribute("dataindex").getText(); if (!StringUtils.isEmpty(fieldName)) { fieldNames.add(fieldName); } } checkCount(jdbc.getDataSource().getConnection(), sql); List> listMap = jdbc.listMap(sql); if (CollectionUtils.isEmpty(listMap)) { continue; } for (Map map : listMap) { // 每一行数据添加一个 data标签 Element dataElement = new DefaultElement("data"); element.add(dataElement); for (String fieldName : fieldNames) { // 每一列在 data 下添加一个标签 Element fieldElement = new DefaultElement(fieldName); fieldElement.setText(toString(map.get(fieldName.toUpperCase()))); dataElement.add(fieldElement); } } } return document.asXML(); } /** * 处理模版中的 bar 和 line 组件 * * @param content * 模版内容 * @param jdbc * NewbieJdbcSupport 对象 * @param globalParameters * 公共参数 * @return 解析后的模版内容 * @throws DocumentException * @throws SQLException * @throws IllegalStateException */ @SuppressWarnings("unchecked") private String processBarAndLine(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List globalParameters) throws DocumentException, IllegalStateException, SQLException { Document document = getDocument(content); // 获取 bar 和 line 组件 List elements = document.selectNodes("//bar | //line"); for (Element element : elements) { // 执行 sql ,获取数据 String sql = element.attribute("sql").getText(); if(StringUtils.isEmpty(sql)){ continue; } sql = replaceParameters(sql, globalParameters, true,jdbc); checkCount(jdbc.getDataSource().getConnection(), sql); Map> map = convert(jdbc.listMap(sql)); if (CollectionUtils.isEmpty(map)) { continue; } // xvalue 字段名 String xvalue = element.attribute("xvalue").getText().toUpperCase(); if (xvalue != null) { // 设置 xvalue element.attribute("xfields").setText(toString(map.remove(xvalue))); } // fields 名称 String[] fields = element.attribute("fields").getText().split(",[\\s]*"); if (ArrayUtils.isEmpty(fields)) { continue; } // 添加 series 节点,用于存储 fields 的值 for (String field : fields) { Element seriesElement = new DefaultElement("series"); seriesElement.addAttribute("name", field); List value = map.remove(field.toUpperCase()); // 对于某一列,其每一行的数据创建一个 data 标签 if (CollectionUtils.isEmpty(value)) { Element dataElement = new DefaultElement("data"); seriesElement.add(dataElement); } else { for (Object object : value) { Element dataElement = new DefaultElement("data"); dataElement.setText(toString(object)); seriesElement.add(dataElement); } } element.add(seriesElement); } } return document.asXML(); } /** * 处理模版中的pie组件 * * @param content * 模版内容 * @param jdbc * NewbieJdbcSupport 对象 * @param globalParameters * 公共参数 * @return 解析后的模版内容 * @throws DocumentException * @throws SQLException * @throws IllegalStateException */ @SuppressWarnings("unchecked") private String processPie(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List globalParameters) throws DocumentException, IllegalStateException, SQLException { Document document = getDocument(content); // 获取 bar 和 line 组件 List elements = document.selectNodes("//pie"); for (Element element : elements) { // 执行 sql ,获取数据 String sql = element.attribute("sql").getText(); if(StringUtils.isEmpty(sql)){ continue; } sql = replaceParameters(sql, globalParameters, true,jdbc); checkCount(jdbc.getDataSource().getConnection(), sql); List> listMap = jdbc.listMap(sql); if (CollectionUtils.isEmpty(listMap)) { continue; } // field 和 value String field = element.attribute("field").getText().toUpperCase(); String value = element.attribute("value").getText().toUpperCase(); for (Map map : listMap) { Element seriesElement = new DefaultElement("series"); seriesElement.addAttribute("name", toString(map.get(field))); seriesElement.addAttribute("data", toString(map.get(value))); element.add(seriesElement); } } return document.asXML(); } /** * 读取 xml 为 Document 对象 * * @param xml * xml 字符串 * @return Document 对象 * @throws DocumentException */ private Document getDocument(@NotEmpty("xml") String xml) throws DocumentException { return new SAXReader().read(new StringReader(xml)); } /** * 检查当前条件下的结果数目是否超出限制 * * @param connection * @param sql * @throws SQLException * @throws IllegalStateException */ private void checkCount(@NotEmpty("connection") Connection connection, @NotEmpty("sql") String sql) throws SQLException, IllegalStateException { int count = getCount(connection, sql); if (count > MAX_RECORD_SIZE) { String message = "查询条件的结果数目超出限制:" + count; throw new IllegalStateException(message, new IllegalStateException("sql : " + sql)); } } /** * 获取当前查询语句的结果数目 * * @param connection * @param sql * @return * @throws SQLException */ private int getCount(@NotEmpty("connection") Connection connection, @NotEmpty("sql") String sql) throws SQLException { PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { // 如果直接在 sql 外用 count(1) 统计数目,当关联表多时,可能会出现错误 // ORA-01792: 表或视图中的最大列数为 1000 // 报错主要发生在 select * 的情况下,但是不能这样判断,因为可能存在 select tt.*, pi_id from // purchase t left join 这样的情况,很难区分 // 因此 1. 对于普通 sql ,将 select 后的字段改为 count(1) // 2. 而最外层含有 group by 的 sql ,直接改为 count(1) 可能得到不止一行,结果也并非实际行数。再加上 // group // by 的结果列数一般很小,所以可以在外面使用 count(1) ,一般不会超出 1000 行 String lowerSql = sql.toLowerCase(); if (!lowerSql.matches("[\\s\\S]+?group[\\s]+?by[\\s]+?[^)]+?")) { String regex = "([\\s\\S]+?from)[\\s]+?[^,]+?"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(lowerSql); if (matcher.find()) { int start = matcher.start(1); int end = matcher.end(1); sql = sql.substring(0, start) + "select count(1) from" + sql.substring(end); } else { throw new IllegalStateException("sql 解析错误:未发现第一个 from:" + sql); } } else { sql = "select count(1) from (" + sql + ")"; } preparedStatement = connection.prepareStatement(sql); resultSet = preparedStatement.executeQuery(); resultSet.next(); int count = resultSet.getInt(1); return count; } finally { if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { logger.error("", e); } } if (preparedStatement != null) { try { preparedStatement.close(); } catch (SQLException e) { logger.error("", e); } } connection.close(); } } /** * 将 List> 转为 Map> * * @param listMap * 将转换的对象,每个 Map 的键都相同,否则会得到非预期的结果
* 如下所示: *

* * * * * * * * * * * * * * * * *
输入输出结果
[{age=12, name=Joe}, {age=31, name=Lee}]{name=[Joe, Lee], age=[12, 31]}预期
[{age=12, name=Joe}, {name=Lee, agee=31}]{name=[Joe, Lee], age=[12], agee=[31]}非预期
* @return 转换的结果 */ private Map> convert(List> listMap) { Map> result = new HashMap<>(); if (CollectionUtils.isEmpty(listMap)) { return result; } for (Map map : listMap) { Set> entrySet = map.entrySet(); for (Entry entry : entrySet) { String key = entry.getKey(); Object value = entry.getValue(); List list = result.get(key); if (list == null) { list = new ArrayList<>(); result.put(key, list); } list.add(value); } } return result; } /** * 将对象转为字符串 * * @param object * 对象 * @return 转换的字符串 */ private String toString(Object object) { if (object == null) { return ""; } if (object instanceof Date) { return String.valueOf(((Date) object).getTime()); } return String.valueOf(object); } /** * 对模版内容的最终处理,包括映射、添加title等 * * @param content * 模版内容 * @param title * 标题 * @return 处理后的模版内容 * @throws IOException * @throws TransformerException * @throws DocumentException */ private String finalProcess(@NotEmpty("content") String content, String title) throws TransformerException, IOException, DocumentException { InputStream mapRuleStream = FileHelper.readStream("map-rule.xsl"); content = TranslateHelper.map(content, mapRuleStream); if (!StringUtils.isEmpty(title)) { content = addTitle(content, title); } return content; } /** * 为模版添加 title * * @param content * 模版内容 * @param title * 标题 * @return 处理后的模版内容 * @throws DocumentException */ private String addTitle(@NotEmpty("content") String content, @NotEmpty("title") String title) throws DocumentException { Document document = getDocument(content); Element rootElement = document.getRootElement(); Element titleElement = new DefaultElement("title"); titleElement.setText(title); rootElement.add(titleElement); return document.asXML(); } }