Bläddra i källkod

Reimplement kanban, panel, panel instance, parameter

sunyj 8 år sedan
förälder
incheckning
dab985bacc
31 ändrade filer med 2414 tillägg och 590 borttagningar
  1. 7 8
      kanban-auth/src/main/java/com/uas/kanban/SecurityConfiguration.java
  2. 5 0
      kanban-auth/src/main/java/com/uas/kanban/dao/UserDao.java
  3. 1 1
      kanban-auth/src/main/java/com/uas/kanban/filter/SecurityInterceptor.java
  4. 5 11
      kanban-auth/src/main/java/com/uas/kanban/service/impl/UserServiceImpl.java
  5. 1 1
      kanban-common/src/main/java/com/uas/kanban/base/BaseController.java
  6. 75 13
      kanban-common/src/main/java/com/uas/kanban/base/BaseDao.java
  7. 22 22
      kanban-common/src/main/java/com/uas/kanban/base/BaseService.java
  8. 40 0
      kanban-console/src/main/java/com/uas/kanban/controller/KanbanController.java
  9. 43 0
      kanban-console/src/main/java/com/uas/kanban/controller/PanelController.java
  10. 70 0
      kanban-console/src/main/java/com/uas/kanban/controller/PanelInstanceController.java
  11. 40 0
      kanban-console/src/main/java/com/uas/kanban/controller/ParameterController.java
  12. 5 0
      kanban-console/src/main/java/com/uas/kanban/dao/DataSourceDao.java
  13. 21 0
      kanban-console/src/main/java/com/uas/kanban/dao/KanbanDao.java
  14. 21 0
      kanban-console/src/main/java/com/uas/kanban/dao/PanelDao.java
  15. 34 0
      kanban-console/src/main/java/com/uas/kanban/dao/PanelInstanceDao.java
  16. 21 0
      kanban-console/src/main/java/com/uas/kanban/dao/ParameterDao.java
  17. 122 0
      kanban-console/src/main/java/com/uas/kanban/model/Kanban.java
  18. 113 0
      kanban-console/src/main/java/com/uas/kanban/model/Panel.java
  19. 181 0
      kanban-console/src/main/java/com/uas/kanban/model/PanelInstance.java
  20. 286 0
      kanban-console/src/main/java/com/uas/kanban/model/Parameter.java
  21. 3 3
      kanban-console/src/main/java/com/uas/kanban/model/Value.java
  22. 32 0
      kanban-console/src/main/java/com/uas/kanban/service/KanbanService.java
  23. 58 0
      kanban-console/src/main/java/com/uas/kanban/service/PanelInstanceService.java
  24. 25 0
      kanban-console/src/main/java/com/uas/kanban/service/PanelService.java
  25. 24 0
      kanban-console/src/main/java/com/uas/kanban/service/ParameterService.java
  26. 94 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanServiceImpl.java
  27. 331 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/PanelInstanceServiceImpl.java
  28. 88 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/PanelServiceImpl.java
  29. 116 0
      kanban-console/src/main/java/com/uas/kanban/service/impl/ParameterServiceImpl.java
  30. 1 4
      kanban-console/src/main/java/com/uas/kanban/support/DataSourceManager.java
  31. 529 527
      kanban-console/src/main/java/com/uas/kanban/support/KanbanParser.java

+ 7 - 8
kanban-auth/src/main/java/com/uas/kanban/SecurityConfiguration.java

@@ -34,15 +34,14 @@ public class SecurityConfiguration extends WebMvcConfigurerAdapter {
         registration.excludePathPatterns("/user/login", "/user/logout", "/user/exist");
 
         // 添加管理员才可访问的路径
-        // 用户、资源点管理
+        // 用户、面板管理
         securityInterceptor.addAdminPatterns("/user/save*/**", "/user/update*/**", "/user/delete*/**", "/user/get*/**",
-                "/resourcePoint/save*/**", "/resourcePoint/update*/**", "/resourcePoint/delete*/**");
-        // 模版设计
-        securityInterceptor.addAdminPatterns("/template/save*/**", "/template/update*/**", "/template/delete*/**",
-                "/board");
-        // 公共参数
-        securityInterceptor.addAdminPatterns("/globalParameters/save*/**", "/globalParameters/update*/**",
-                "/globalParameters/delete*/**");
+                "/panel/**");
+        // 看板设计
+        securityInterceptor.addAdminPatterns("/kanban/**", "/board");
+        // 参数
+        securityInterceptor.addAdminPatterns("/parameter/save*/**", "/parameter/update*/**",
+                "/parameter/delete*/**");
         // 数据源
         securityInterceptor.addAdminPatterns("/datasource/save*/**", "/datasource/update*/**",
                 "/datasource/delete*/**");

+ 5 - 0
kanban-auth/src/main/java/com/uas/kanban/dao/UserDao.java

@@ -13,4 +13,9 @@ import org.springframework.stereotype.Component;
 @Component
 public class UserDao extends BaseDao<User> {
 
+    @Override
+    protected String collectionSimpleName() {
+        return "用户";
+    }
+
 }

+ 1 - 1
kanban-auth/src/main/java/com/uas/kanban/filter/SecurityInterceptor.java

@@ -80,7 +80,7 @@ public class SecurityInterceptor extends HandlerInterceptorAdapter {
         if (user.getRole() != Role.Admin) {
             for (String pattern : this.adminPatterns) {
                 if (pathMatcher.match(pattern, url)) {
-                    throw new OperationException("没有权限");
+                    throw new OperationException("权限不足");
                 }
             }
         }

+ 5 - 11
kanban-auth/src/main/java/com/uas/kanban/service/impl/UserServiceImpl.java

@@ -50,10 +50,7 @@ public class UserServiceImpl extends BaseService<User> implements UserService {
     public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
         User user = userDao.parse(json);
         String code = user.codeNotEmpty();
-        User oldUser = userDao.findOne(code);
-        if (oldUser == null) {
-            throw new IllegalStateException("用户不存在");
-        }
+        User oldUser = userDao.checkExist(code);
         if (oldUser.getRole() == Role.Admin) {
             if (!Objects.equals(code, SystemSession.checkUser().getCode())) {
                 throw new OperationException("不允许更改其他管理员:" + oldUser.getName());
@@ -110,11 +107,11 @@ public class UserServiceImpl extends BaseService<User> implements UserService {
 
     @Override
     public boolean resetPassword(@NotEmpty("password") String password, @NotEmpty("newPassword") String newPassword) {
-        User rPoint = SystemSession.checkUser();
-        String code = rPoint.getCode();
+        User userInSession = SystemSession.checkUser();
+        String code = userInSession.getCode();
         User user = userDao.findOne(code);
         if (user == null) {
-            throw new IllegalStateException("用户不存在:" + rPoint);
+            throw new IllegalStateException("用户不存在:" + userInSession);
         }
         if (!Objects.equals(password, user.getPassword())) {
             throw new IllegalStateException("旧密码错误");
@@ -161,10 +158,7 @@ public class UserServiceImpl extends BaseService<User> implements UserService {
      * @throws OperationException 更改其他管理员
      */
     private void checkAdmin(@NotEmpty("code") String code) throws OperationException {
-        User user = userDao.findOne(code);
-        if (user == null) {
-            throw new IllegalStateException("用户不存在");
-        }
+        User user = userDao.checkExist(code);
         if (user.getRole() == Role.Admin) {
             if (!Objects.equals(code, SystemSession.checkUser().getCode())) {
                 throw new OperationException("不允许删除其他管理员:" + user.getName());

+ 1 - 1
kanban-common/src/main/java/com/uas/kanban/base/BaseController.java

@@ -57,7 +57,7 @@ public abstract class BaseController<T extends BaseEntity> {
      */
     @RequestMapping("/delete/all")
     @ResponseBody
-    public int deleteAll(HttpServletRequest request) {
+    public int deleteAll(HttpServletRequest request) throws OperationException {
         return baseService.deleteAll();
     }
 

+ 75 - 13
kanban-common/src/main/java/com/uas/kanban/base/BaseDao.java

@@ -16,7 +16,10 @@ import org.mongodb.morphia.Datastore;
 import org.mongodb.morphia.Key;
 import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Reference;
-import org.mongodb.morphia.query.*;
+import org.mongodb.morphia.query.FindOptions;
+import org.mongodb.morphia.query.Query;
+import org.mongodb.morphia.query.UpdateOperations;
+import org.mongodb.morphia.query.UpdateResults;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.lang.reflect.Field;
@@ -162,6 +165,18 @@ public class BaseDao<T extends BaseEntity> {
         return operations;
     }
 
+    /**
+     * 利用反射获取指定对象的指定字段的值
+     *
+     * @param field 指定字段
+     * @param k     指定对象
+     * @return 指定字段的值
+     */
+    private <K> Object getValue(String field, @NotEmpty("k") K k) throws IllegalStateException, NoSuchFieldException {
+        Field declaredField = k.getClass().getDeclaredField(field);
+        return getValue(declaredField, k);
+    }
+
     /**
      * 利用反射获取指定对象的指定字段的值
      *
@@ -466,12 +481,34 @@ public class BaseDao<T extends BaseEntity> {
      *
      * @param code the code to query
      * @return the matched entity. may be null.
-     * @throws IllegalStateException 数据超过1条
+     * @throws IllegalStateException more than one
      */
     public T findOne(@NotEmpty("code") String code) throws IllegalStateException {
         return findBy("code", code);
     }
 
+    /**
+     * Check the given entity (by code)
+     *
+     * @param code the code to query
+     * @return the matched entity.
+     * @throws IllegalStateException more than one or not exist
+     */
+    public T checkExist(@NotEmpty("code") String code) throws IllegalStateException {
+        T t = findOne(code);
+        if (t == null) {
+            throw new IllegalStateException(collectionSimpleName() + "不存在:" + code);
+        }
+        return t;
+    }
+
+    /**
+     * @return 集合简称
+     */
+    protected String collectionSimpleName() {
+        return entityClass.getSimpleName();
+    }
+
     /**
      * Find the given entities (by code);
      *
@@ -512,14 +549,44 @@ public class BaseDao<T extends BaseEntity> {
     /**
      * Execute the query and get the results
      *
-     * @param query the query used to match the documents to update
+     * @param query the query used to match the documents
      * @return The results
      */
     public List<T> find(@NotEmpty("query") Query<T> query) {
-        List<T> result = new ArrayList<>();
-        MorphiaIterator<T, T> morphiaIterator = query.fetch();
-        while (morphiaIterator.hasNext()) {
-            result.add(morphiaIterator.next());
+        return query.asList();
+    }
+
+    /**
+     * Execute the query and get the results (Limits the field retrieved)
+     *
+     * @param query the query used to match the documents
+     * @param field retrieved field
+     * @param clazz type of the retrieved field
+     * @param <K>   type of the retrieved field
+     * @return The results
+     * @throws NoSuchFieldException
+     */
+    public <K> List<K> findField(@NotEmpty("query") Query<T> query, @NotEmpty("field") String field, Class<K> clazz) throws NoSuchFieldException {
+        query.project(field, true);
+        List<T> list = query.asList();
+        List<K> result = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(list)) {
+            for (T t : list) {
+                try {
+                    Object value = getValue(field, t);
+                    if (value != null) {
+                        Class<?> valueClass = value.getClass();
+                        if (clazz.isAssignableFrom(valueClass)) {
+                            result.add((K) value);
+                        } else {
+                            throw new ClassCastException("字段 " + field + " 的类型为 " + valueClass + " , 无法转为 " + clazz);
+                        }
+                    }
+                } catch (NoSuchFieldException e) {
+                    throw new NoSuchFieldException(entityClass + " 中不存在字段:" + field);
+                }
+
+            }
         }
         return result;
     }
@@ -581,12 +648,7 @@ public class BaseDao<T extends BaseEntity> {
         FindOptions findOptions = new FindOptions();
         findOptions.skip(offset);
         findOptions.limit(size);
-        MorphiaIterator<T, T> morphiaIterator = query.fetch(findOptions);
-        List<T> content = new ArrayList<>();
-        while (morphiaIterator.hasNext()) {
-            content.add(morphiaIterator.next());
-        }
-        result.setContent(content);
+        result.setContent(query.asList());
         return result;
     }
 

+ 22 - 22
kanban-common/src/main/java/com/uas/kanban/base/BaseService.java

@@ -40,12 +40,33 @@ public abstract class BaseService<T extends BaseEntity> {
         return baseDao.savePart(t);
     }
 
+    /**
+     * 根据 code 更新数据
+     *
+     * @param json json格式的数据
+     * @return 更新的数据条数
+     */
+    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        T t = baseDao.parse(json);
+        return baseDao.update(t);
+    }
+
+    /**
+     * 更新部分数据(不对数据的非空等做检查)
+     *
+     * @param json json格式的数据
+     * @return 更新的数据条数
+     */
+    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        return baseDao.updatePart(json);
+    }
+
     /**
      * 删除所有数据
      *
      * @return 删除的数据条数
      */
-    public int deleteAll() {
+    public int deleteAll() throws OperationException {
         return baseDao.deleteAll();
     }
 
@@ -69,27 +90,6 @@ public abstract class BaseService<T extends BaseEntity> {
         return baseDao.delete(codes);
     }
 
-    /**
-     * 根据 code 更新数据
-     *
-     * @param json json格式的数据
-     * @return 更新的数据条数
-     */
-    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
-        T t = baseDao.parse(json);
-        return baseDao.update(t);
-    }
-
-    /**
-     * 更新部分数据(不对数据的非空等做检查)
-     *
-     * @param json json格式的数据
-     * @return 更新的数据条数
-     */
-    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
-        return baseDao.updatePart(json);
-    }
-
     /**
      * 获取所有数据
      *

+ 40 - 0
kanban-console/src/main/java/com/uas/kanban/controller/KanbanController.java

@@ -0,0 +1,40 @@
+package com.uas.kanban.controller;
+
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.Kanban;
+import com.uas.kanban.service.KanbanService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 看板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:22
+ */
+@Controller
+@RequestMapping("/kanban")
+public class KanbanController extends BaseController<Kanban> {
+
+    @Autowired
+    private KanbanService kanbanService;
+
+    /**
+     * 获取指定面板的看板
+     *
+     * @param panelCode 所指定的面板 code
+     * @param request   request
+     * @return 看板
+     */
+    @RequestMapping("/get/byPanel/{panelCode}")
+    @ResponseBody
+    public List<Kanban> getByPanelCode(@PathVariable("panelCode") String panelCode, HttpServletRequest request) {
+        return kanbanService.getByPanelCode(panelCode);
+    }
+}

+ 43 - 0
kanban-console/src/main/java/com/uas/kanban/controller/PanelController.java

@@ -0,0 +1,43 @@
+package com.uas.kanban.controller;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.Panel;
+import com.uas.kanban.model.PanelInstance;
+import com.uas.kanban.service.PanelService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 面板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:22
+ */
+@Controller
+@RequestMapping("/panel")
+public class PanelController extends BaseController<Panel> {
+
+    @Autowired
+    private PanelService panelService;
+
+    /**
+     * 为指定用户分配面板
+     *
+     * @param userCode   用户 code
+     * @param panelCodes 面板 code
+     * @param request    request
+     * @return 面板实例
+     */
+    @RequestMapping("/assignPanel")
+    @ResponseBody
+    public List<PanelInstance> assignPanel(@NotEmpty("userCode") String userCode, @NotEmpty("panelCodes") List<String> panelCodes, HttpServletRequest request) {
+        return panelService.assignPanel(userCode, panelCodes);
+    }
+
+}

+ 70 - 0
kanban-console/src/main/java/com/uas/kanban/controller/PanelInstanceController.java

@@ -0,0 +1,70 @@
+package com.uas.kanban.controller;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.PanelInstance;
+import com.uas.kanban.service.PanelInstanceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 面板实例
+ *
+ * @author sunyj
+ * @since 2017/10/19 11:40
+ */
+@Controller
+@RequestMapping("/panelInstance")
+public class PanelInstanceController {
+
+    @Autowired
+    private PanelInstanceService panelInstanceService;
+
+    /**
+     * 根据指定的面板实例,解析生成json数据传给前台
+     *
+     * @param code       面板实例 code
+     * @param kanbanCode 看板 code,可为空,为空时解析第一个看板数据
+     * @param request    request
+     * @return 解析生成的json
+     */
+    @RequestMapping("/parseData/{code}")
+    @ResponseBody
+    public Map<String, Object> parseData(@NotEmpty("code") String code, String kanbanCode,
+                                         HttpServletRequest request) {
+        return panelInstanceService.parseData(code, kanbanCode);
+    }
+
+    /**
+     * 获取指定面板的实例
+     *
+     * @param panelCode 所指定的面板 code
+     * @param request   request
+     * @return 面板实例
+     */
+    @RequestMapping("/get/byPanel/{panelCode}")
+    @ResponseBody
+    public List<PanelInstance> getByPanelCode(@PathVariable("panelCode") String panelCode, HttpServletRequest request) {
+        return panelInstanceService.getByPanelCode(panelCode);
+    }
+
+    /**
+     * 将实例保存到桌面
+     *
+     * @param json    json格式的数据
+     * @param request request
+     * @return 保存的实例
+     */
+    @RequestMapping("/save/toDesktop")
+    @ResponseBody
+    public PanelInstance saveToDesktop(@NotEmpty("json") String json, HttpServletRequest request) {
+        return panelInstanceService.saveToDesktop(json);
+    }
+
+}

+ 40 - 0
kanban-console/src/main/java/com/uas/kanban/controller/ParameterController.java

@@ -0,0 +1,40 @@
+package com.uas.kanban.controller;
+
+import com.uas.kanban.base.BaseController;
+import com.uas.kanban.model.Parameter;
+import com.uas.kanban.service.ParameterService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 参数
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:22
+ */
+@Controller
+@RequestMapping("/parameter")
+public class ParameterController extends BaseController<Parameter> {
+
+    @Autowired
+    private ParameterService parameterService;
+
+    /**
+     * 获取指定面板的参数
+     *
+     * @param panelCode 所指定的面板 code
+     * @param request   request
+     * @return 参数
+     */
+    @RequestMapping("/get/byPanel/{panelCode}")
+    @ResponseBody
+    public List<Parameter> getByPanelCode(@PathVariable("panelCode") String panelCode, HttpServletRequest request) {
+        return parameterService.getByPanelCode(panelCode);
+    }
+}

+ 5 - 0
kanban-console/src/main/java/com/uas/kanban/dao/DataSourceDao.java

@@ -13,4 +13,9 @@ import org.springframework.stereotype.Component;
 @Component
 public class DataSourceDao extends BaseDao<DataSource> {
 
+    @Override
+    protected String collectionSimpleName() {
+        return "数据源";
+    }
+
 }

+ 21 - 0
kanban-console/src/main/java/com/uas/kanban/dao/KanbanDao.java

@@ -0,0 +1,21 @@
+package com.uas.kanban.dao;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.Kanban;
+import org.springframework.stereotype.Component;
+
+/**
+ * 看板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:24
+ */
+@Component
+public class KanbanDao extends BaseDao<Kanban> {
+
+    @Override
+    protected String collectionSimpleName() {
+        return "看板";
+    }
+
+}

+ 21 - 0
kanban-console/src/main/java/com/uas/kanban/dao/PanelDao.java

@@ -0,0 +1,21 @@
+package com.uas.kanban.dao;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.Panel;
+import org.springframework.stereotype.Component;
+
+/**
+ * 面板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:21
+ */
+@Component
+public class PanelDao extends BaseDao<Panel> {
+
+    @Override
+    protected String collectionSimpleName() {
+        return "面板";
+    }
+
+}

+ 34 - 0
kanban-console/src/main/java/com/uas/kanban/dao/PanelInstanceDao.java

@@ -0,0 +1,34 @@
+package com.uas.kanban.dao;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.PanelInstance;
+import com.uas.kanban.model.User;
+import com.uas.kanban.support.SystemSession;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 面板实例
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:21
+ */
+@Component
+public class PanelInstanceDao extends BaseDao<PanelInstance> {
+
+    @Override
+    protected Map<String, Object> globalFilter() {
+        User user = SystemSession.checkUser();
+        Map<String, Object> filters = new HashMap<>();
+        // 普通用户只能操作自己的实例
+        filters.put("userCode", user.getCode());
+        return filters;
+    }
+
+    @Override
+    protected String collectionSimpleName() {
+        return "面板实例";
+    }
+}

+ 21 - 0
kanban-console/src/main/java/com/uas/kanban/dao/ParameterDao.java

@@ -0,0 +1,21 @@
+package com.uas.kanban.dao;
+
+import com.uas.kanban.base.BaseDao;
+import com.uas.kanban.model.Parameter;
+import org.springframework.stereotype.Component;
+
+/**
+ * 参数
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:24
+ */
+@Component
+public class ParameterDao extends BaseDao<Parameter> {
+
+    @Override
+    protected String collectionSimpleName() {
+        return "参数";
+    }
+
+}

+ 122 - 0
kanban-console/src/main/java/com/uas/kanban/model/Kanban.java

@@ -0,0 +1,122 @@
+package com.uas.kanban.model;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.IndexOptions;
+import org.mongodb.morphia.annotations.Indexed;
+
+/**
+ * 看板
+ *
+ * @author sunyj
+ * @since 2017/10/18 15:35
+ */
+@Entity
+public class Kanban extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 名称
+     */
+    @Indexed(options = @IndexOptions(unique = true))
+    @FieldProperty(nullable = false)
+    private String name;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 描述
+     */
+    private String description;
+
+    /**
+     * 内容
+     */
+    @FieldProperty(nullable = false)
+    private String content;
+
+    /**
+     * 是否启用
+     */
+    @FieldProperty(nullable = false)
+    private Boolean enabled;
+
+    /**
+     * 面板 code {@link Panel}
+     */
+    @FieldProperty(nullable = false)
+    private String panelCode;
+
+    @Override
+    public void init() {
+        if (enabled == null) {
+            enabled = true;
+        }
+        super.init();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public String getPanelCode() {
+        return panelCode;
+    }
+
+    public void setPanelCode(String panelCode) {
+        this.panelCode = panelCode;
+    }
+
+    @Override
+    public String toString() {
+        return "Kanban{" +
+                "name='" + name + '\'' +
+                ", title='" + title + '\'' +
+                ", description='" + description + '\'' +
+                ", content='" + content + '\'' +
+                ", enabled=" + enabled +
+                ", panelCode='" + panelCode + '\'' +
+                "} " + super.toString();
+    }
+}

+ 113 - 0
kanban-console/src/main/java/com/uas/kanban/model/Panel.java

@@ -0,0 +1,113 @@
+package com.uas.kanban.model;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.IndexOptions;
+import org.mongodb.morphia.annotations.Indexed;
+
+/**
+ * 面板
+ *
+ * @author sunyj
+ * @since 2017/10/18 15:35
+ */
+@Entity
+public class Panel extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 名称
+     */
+    @Indexed(options = @IndexOptions(unique = true))
+    @FieldProperty(nullable = false)
+    private String name;
+
+    /**
+     * 展示方式
+     */
+    @FieldProperty(nullable = false)
+    private Display display;
+
+    /**
+     * 图标样式
+     */
+    private String iconCls;
+
+    /**
+     * 数据源 code {@link DataSource} <br/>
+     * 不使用 @Reference,原因请见 {@link BaseEntity#id}
+     */
+    @FieldProperty(nullable = false)
+    private String dataSourceCode;
+
+    @Override
+    public void init() {
+        if (display == null) {
+            display = Display.AutoSwitch;
+        }
+        super.init();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Display getDisplay() {
+        return display;
+    }
+
+    public void setDisplay(Display display) {
+        this.display = display;
+    }
+
+    public String getIconCls() {
+        return iconCls;
+    }
+
+    public void setIconCls(String iconCls) {
+        this.iconCls = iconCls;
+    }
+
+    public String getDataSourceCode() {
+        return dataSourceCode;
+    }
+
+    public void setDataSourceCode(String dataSourceCode) {
+        this.dataSourceCode = dataSourceCode;
+    }
+
+    @Override
+    public String toString() {
+        return "Panel{" +
+                "name='" + name + '\'' +
+                ", display=" + display +
+                ", iconCls='" + iconCls + '\'' +
+                ", dataSourceCode='" + dataSourceCode + '\'' +
+                "} " + super.toString();
+    }
+
+    /**
+     * 展示方式
+     *
+     * @author sunyj
+     * @since 2017年9月1日 下午8:01:53
+     */
+    public enum Display {
+
+        /**
+         * 自动切换
+         */
+        AutoSwitch,
+
+        /**
+         * 分屏展示
+         */
+        SplitScreen
+    }
+}

+ 181 - 0
kanban-console/src/main/java/com/uas/kanban/model/PanelInstance.java

@@ -0,0 +1,181 @@
+package com.uas.kanban.model;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+import com.uas.kanban.support.SystemSession;
+import com.uas.kanban.util.CollectionUtils;
+import com.uas.kanban.util.ObjectUtils;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Entity;
+
+import java.util.List;
+
+/**
+ * 面板实例
+ *
+ * @author sunyj
+ * @since 2017/10/18 15:58
+ */
+@Entity
+public class PanelInstance extends BaseEntity {
+
+    /**
+     * 默认切换频率为 10 s
+     */
+    public static final double DEFAULT_SWITCH_FREQUENCY = 10;
+    /**
+     * 默认刷新频率为 5 s
+     */
+    public static final double DEFAULT_REFRESH_FREQUENCY = 5;
+    private static final long serialVersionUID = 1L;
+    /**
+     * 名称
+     */
+    @FieldProperty(nullable = false)
+    private String name;
+
+    /**
+     * 图标样式
+     */
+    private String iconCls;
+
+    /**
+     * 切换频率(秒)面板的展示方式为 {@link Panel.Display#AutoSwitch} 时,才生效)
+     */
+    private Double switchFrequency;
+
+    /**
+     * 刷新频率(秒)
+     */
+    @FieldProperty(nullable = false)
+    private Double refreshFrequency;
+
+    /**
+     * 是否自动生成,如果是的话,普通用户不能删除
+     */
+    private Boolean autoGenerated;
+
+    /**
+     * 参数
+     */
+    @Embedded
+    private JSONArray parameters;
+
+    /**
+     * 面板 code {@link Panel}
+     */
+    @FieldProperty(nullable = false)
+    private String panelCode;
+
+    /**
+     * 用户 code {@link User}
+     */
+    @FieldProperty(nullable = false)
+    private String userCode;
+
+    @Override
+    public void init() {
+        User user = SystemSession.checkUser();
+        userCode = user.getCode();
+        if (refreshFrequency == null) {
+            refreshFrequency = DEFAULT_REFRESH_FREQUENCY;
+        }
+        super.init();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getIconCls() {
+        return iconCls;
+    }
+
+    public void setIconCls(String iconCls) {
+        this.iconCls = iconCls;
+    }
+
+    public Double getSwitchFrequency() {
+        return switchFrequency;
+    }
+
+    public void setSwitchFrequency(Double switchFrequency) {
+        this.switchFrequency = switchFrequency;
+    }
+
+    public Double getRefreshFrequency() {
+        return refreshFrequency;
+    }
+
+    public void setRefreshFrequency(Double refreshFrequency) {
+        this.refreshFrequency = refreshFrequency;
+    }
+
+    public Boolean getAutoGenerated() {
+        return autoGenerated;
+    }
+
+    public void setAutoGenerated(Boolean autoGenerated) {
+        this.autoGenerated = autoGenerated;
+    }
+
+    public JSONArray getParameters() {
+        return parameters;
+    }
+
+    public void setParameters(JSONArray parameters) {
+        this.parameters = parameters;
+    }
+
+    public String getPanelCode() {
+        return panelCode;
+    }
+
+    public void setPanelCode(String panelCode) {
+        this.panelCode = panelCode;
+    }
+
+    public String getUserCode() {
+        return userCode;
+    }
+
+    public void setUserCode(String userCode) {
+        this.userCode = userCode;
+    }
+
+    @Override
+    public String toString() {
+        return "PanelInstance{" +
+                "name='" + name + '\'' +
+                ", iconCls='" + iconCls + '\'' +
+                ", switchFrequency=" + switchFrequency +
+                ", refreshFrequency=" + refreshFrequency +
+                ", autoGenerated=" + autoGenerated +
+                ", parameters=" + parameters +
+                ", panelCode='" + panelCode + '\'' +
+                ", userCode='" + userCode + '\'' +
+                "} " + super.toString();
+    }
+
+    /**
+     * 获取参数
+     */
+    public List<Parameter> fromParameters() {
+        return ObjectUtils.toList(parameters, Parameter.class);
+    }
+
+    /**
+     * 设置参数
+     */
+    public void toParameters(List<Parameter> parameters) {
+        if (!CollectionUtils.isEmpty(parameters)) {
+            this.parameters = JSONObject.parseArray(JSONObject.toJSONString(parameters));
+        }
+    }
+}

+ 286 - 0
kanban-console/src/main/java/com/uas/kanban/model/Parameter.java

@@ -0,0 +1,286 @@
+package com.uas.kanban.model;
+
+import com.uas.kanban.annotation.FieldProperty;
+import com.uas.kanban.base.BaseEntity;
+import com.uas.kanban.util.CollectionUtils;
+import com.uas.kanban.util.ObjectUtils;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.IndexOptions;
+import org.mongodb.morphia.annotations.Indexed;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 参数
+ *
+ * @author sunyj
+ * @since 2017/10/18 15:35
+ */
+@Entity
+public class Parameter extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 临时变量,用来存储 value(防止 type 未初始化时,value 无法设置)
+     */
+    private transient Object tempValue;
+
+    /**
+     * 临时变量,用来存储 optionalValues(防止 type 未初始化时,value 无法设置)
+     */
+    private transient List<Object> tempOptionalValues;
+
+    /**
+     * 名称,需要和看板中保持一致
+     */
+    @Indexed(options = @IndexOptions(unique = true))
+    @FieldProperty(nullable = false)
+    private String name;
+
+    /**
+     * 类型
+     */
+    @FieldProperty(nullable = false)
+    private Type type;
+
+    /**
+     * 输入方式
+     */
+    @FieldProperty(nullable = false)
+    private InputMode inputMode;
+
+    /**
+     * 输入方式为 {@link InputMode#DropDownBox}
+     * 时,可选择的值,此时不可为空
+     */
+    @Embedded
+    private List<Value> optionalValues;
+
+    /**
+     * 输入方式为 {@link InputMode#DropDownBox} 时,默认值的序号
+     */
+    private Integer defaultOptionalValueIndex;
+
+    /**
+     * 应用面板时,选择或者输入的值
+     */
+    @Embedded
+    private Value value;
+
+    /**
+     * 面板 code {@link Panel}
+     */
+    @FieldProperty(nullable = false)
+    private String panelCode;
+
+    @Override
+    public void init() {
+        if (!ObjectUtils.isEmpty(getValue())) {
+            throw new IllegalArgumentException("不能指定value:" + this.toString());
+        }
+        super.init();
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+        setValue(tempValue);
+        setOptionalValues(tempOptionalValues);
+    }
+
+    public InputMode getInputMode() {
+        return inputMode;
+    }
+
+    public void setInputMode(InputMode inputMode) {
+        this.inputMode = inputMode;
+    }
+
+    public Integer getDefaultOptionalValueIndex() {
+        return defaultOptionalValueIndex;
+    }
+
+    public void setDefaultOptionalValueIndex(Integer defaultOptionalValueIndex) {
+        this.defaultOptionalValueIndex = defaultOptionalValueIndex;
+    }
+
+    public String getPanelCode() {
+        return panelCode;
+    }
+
+    public void setPanelCode(String panelCode) {
+        this.panelCode = panelCode;
+    }
+
+    @Override
+    public String toString() {
+        return "Parameter{" +
+                ", name='" + name + '\'' +
+                ", type=" + type +
+                ", inputMode=" + inputMode +
+                ", optionalValues=" + optionalValues +
+                ", defaultOptionalValueIndex=" + defaultOptionalValueIndex +
+                ", value=" + value +
+                "panelCode='" + panelCode + '\'' +
+                "} " + super.toString();
+    }
+
+    /**
+     * 对于 optionalValues 的操作需注意,因为通过该方法得到的 optionalValues 与 this.optionalValues
+     * 实际上是两个独立的对象,所以更改了返回值 optionalValues 之后,需要重新调用该方法以便应用更改
+     *
+     * @return 可选择的值
+     */
+    public List<Object> getOptionalValues() {
+        if (CollectionUtils.isEmpty(optionalValues)) {
+            return null;
+        }
+        checkType();
+        List<Object> values = new ArrayList<>();
+        for (Value v : optionalValues) {
+            values.add(v == null ? null : v.getValue(type));
+        }
+        return values;
+    }
+
+    /**
+     * 对于 optionalValues 的操作需注意,因为该方法的参数 optionalValues 与 this.optionalValues
+     * 实际上是两个独立的对象,更改参数 optionalValues 之后,需要重新调用该方法以便应用更改
+     *
+     * @param optionalValues 可选择的值
+     */
+    public void setOptionalValues(List<Object> optionalValues) {
+        if (type == null) {
+            tempOptionalValues = optionalValues;
+            return;
+        }
+        if (!CollectionUtils.isEmpty(optionalValues)) {
+            this.optionalValues = new ArrayList<>();
+            for (Object object : optionalValues) {
+                Value v = new Value();
+                v.setValue(type, object);
+                this.optionalValues.add(v);
+            }
+        }
+
+    }
+
+    public Object getValue() {
+        if (value == null) {
+            return null;
+        }
+        checkType();
+        return value.getValue(type);
+    }
+
+    public void setValue(Object value) {
+        if (type == null) {
+            tempValue = value;
+            return;
+        }
+        if (this.value == null) {
+            this.value = new Value();
+        }
+        this.value.setValue(type, value);
+    }
+
+    private void checkType() {
+        if (type == null) {
+            throw new IllegalStateException("type 为空:" + this);
+        }
+    }
+
+    /**
+     * 初始化参数值(如果已填写,不初始化)
+     */
+    public void mayInitValue() {
+        if (!ObjectUtils.isEmpty(getValue())) {
+            return;
+        }
+        if (inputMode == InputMode.DropDownBox && optionalValues != null) {
+            try {
+                value = ObjectUtils.clone(optionalValues.get(defaultOptionalValueIndex == null ? 0 : defaultOptionalValueIndex - 1));
+            } catch (IOException | ClassNotFoundException e) {
+                throw new IllegalStateException("初始化参数出错", e);
+            }
+        }
+    }
+
+    /**
+     * 检查参数
+     *
+     * @throws IllegalArgumentException 必填参数没有填写时,报错
+     */
+    public void checkValue() throws IllegalArgumentException {
+        // 检查参数值是否已填写
+        if (ObjectUtils.isEmpty(getValue())) {
+            if (inputMode == InputMode.DropDownBox && optionalValues != null) {
+                value = optionalValues.get(defaultOptionalValueIndex == null ? 0 : defaultOptionalValueIndex - 1);
+            } else {
+                throw new IllegalArgumentException("需填写参数:code=" + code + ", name=" + name);
+            }
+        }
+        // 通过单选或下拉框方式填写参数时,参数值必须是可选项中的一个值
+        if (inputMode == InputMode.DropDownBox && optionalValues != null) {
+            for (Object obj : optionalValues) {
+                if (Objects.equals(obj, value)) {
+                    return;
+                }
+            }
+        }
+        throw new IllegalArgumentException("参数值 " + value + " 不属于 " + optionalValues);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass() || !(obj instanceof Parameter)) {
+            return false;
+        }
+        Parameter other = (Parameter) obj;
+        return Objects.equals(code, other.getCode()) && Objects.equals(name, other.getName())
+                && Objects.equals(type, other.getType()) && Objects.equals(inputMode, other.getInputMode())
+                && Objects.equals(getOptionalValues(), other.getOptionalValues())
+                && Objects.equals(defaultOptionalValueIndex, other.getDefaultOptionalValueIndex())
+                && Objects.equals(getValue(), other.getValue());
+    }
+
+    /**
+     * 输入方式
+     *
+     * @author sunyj
+     * @since 2017年9月1日 下午8:01:53
+     */
+    public enum InputMode {
+
+        /**
+         * 下拉框
+         */
+        DropDownBox,
+
+        /**
+         * 手输
+         */
+        Manual
+
+    }
+
+}

+ 3 - 3
kanban-console/src/main/java/com/uas/kanban/model/Value.java

@@ -17,17 +17,17 @@ public class Value implements Serializable {
     private static final long serialVersionUID = 1L;
 
     /**
-     * 应用模版时,参数的值(字符串型,根据 {@link Type} 决定取哪个值)
+     * 应用面板时,参数的值(字符串型,根据 {@link Type} 决定取哪个值)
      */
     private String stringValue;
 
     /**
-     * 应用模版时,参数的值(数值型,根据 {@link Type} 决定取哪个值)
+     * 应用面板时,参数的值(数值型,根据 {@link Type} 决定取哪个值)
      */
     private Double numberValue;
 
     /**
-     * 应用模版时,参数的值(日期型,根据 {@link Type} 决定取哪个值)
+     * 应用面板时,参数的值(日期型,根据 {@link Type} 决定取哪个值)
      */
     private Date dateValue;
 

+ 32 - 0
kanban-console/src/main/java/com/uas/kanban/service/KanbanService.java

@@ -0,0 +1,32 @@
+package com.uas.kanban.service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.Kanban;
+
+import java.util.List;
+
+/**
+ * 看板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:31
+ */
+public interface KanbanService {
+
+    /**
+     * 获取指定面板的看板
+     *
+     * @param panelCode 所指定的面板 code
+     * @return 看板
+     */
+    List<Kanban> getByPanelCode(@NotEmpty("panelCode") String panelCode);
+
+    /**
+     * 获取指定面板已启用的看板 code
+     *
+     * @param panelCode 所指定的面板 code
+     * @return 看板 code
+     */
+    List<String> getEnabledCodesByPanelCode(@NotEmpty("panelCode") String panelCode);
+
+}

+ 58 - 0
kanban-console/src/main/java/com/uas/kanban/service/PanelInstanceService.java

@@ -0,0 +1,58 @@
+package com.uas.kanban.service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.PanelInstance;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 面板实例
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:31
+ */
+public interface PanelInstanceService {
+
+    /**
+     * 根据指定的面板实例,解析生成json数据传给前台
+     *
+     * @param code       面板实例 code
+     * @param kanbanCode 看板 code,可为空,为空时解析第一个看板数据
+     * @return 解析生成的json
+     */
+    Map<String, Object> parseData(@NotEmpty("code") String code, String kanbanCode);
+
+    /**
+     * 删除指定面板的实例
+     *
+     * @param panelCodes 所指定的面板 code
+     * @return 删除的数据条数
+     */
+    int deleteByPanelCodes(@NotEmpty("panelCodes") List<String> panelCodes);
+
+    /**
+     * 获取指定面板的实例
+     *
+     * @param panelCode 所指定的面板 code
+     * @return 面板实例
+     */
+    List<PanelInstance> getByPanelCode(@NotEmpty("panelCode") String panelCode);
+
+    /**
+     * 根据指定的面板和用户生成面板实例
+     *
+     * @param userCodes  所指定的用户 code
+     * @param panelCodes 所指定的面板 code
+     * @return 面板实例
+     */
+    List<PanelInstance> generateInstances(@NotEmpty("userCodes") List<String> userCodes, @NotEmpty("panelCodes") List<String> panelCodes);
+
+    /**
+     * 将实例保存到桌面
+     *
+     * @param json json格式的数据
+     * @return 保存的实例
+     */
+    PanelInstance saveToDesktop(@NotEmpty("json") String json);
+}

+ 25 - 0
kanban-console/src/main/java/com/uas/kanban/service/PanelService.java

@@ -0,0 +1,25 @@
+package com.uas.kanban.service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.PanelInstance;
+
+import java.util.List;
+
+/**
+ * 面板
+ *
+ * @author sunyj
+ * @since 2017/10/19 14:28
+ */
+public interface PanelService {
+
+    /**
+     * 为指定用户分配面板
+     *
+     * @param userCode   用户 code
+     * @param panelCodes 面板 code
+     * @return 面板实例
+     */
+    List<PanelInstance> assignPanel(@NotEmpty("userCode") String userCode, @NotEmpty("panelCodes") List<String> panelCodes);
+
+}

+ 24 - 0
kanban-console/src/main/java/com/uas/kanban/service/ParameterService.java

@@ -0,0 +1,24 @@
+package com.uas.kanban.service;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.Parameter;
+
+import java.util.List;
+
+/**
+ * 参数
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:31
+ */
+public interface ParameterService {
+
+    /**
+     * 获取指定面板的参数
+     *
+     * @param panelCode 所指定的面板 code
+     * @return 参数
+     */
+    List<Parameter> getByPanelCode(@NotEmpty("panelCode") String panelCode);
+
+}

+ 94 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/KanbanServiceImpl.java

@@ -0,0 +1,94 @@
+package com.uas.kanban.service.impl;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.KanbanDao;
+import com.uas.kanban.dao.PanelDao;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.Kanban;
+import com.uas.kanban.service.KanbanService;
+import org.mongodb.morphia.query.Query;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 看板
+ *
+ * @author sunyj
+ * @since 2017/10/18 15:43
+ */
+@Service
+public class KanbanServiceImpl extends BaseService<Kanban> implements KanbanService {
+
+    @Autowired
+    private KanbanDao kanbanDao;
+
+    @Autowired
+    private PanelDao panelDao;
+
+    @Override
+    public Kanban save(@NotEmpty("json") String json) {
+        // TODO update related Panel and PanelInstance
+        Kanban kanban = kanbanDao.parse(json);
+        panelDao.checkExist(kanban.getPanelCode());
+        return kanbanDao.save(kanban);
+    }
+
+    @Override
+    public Kanban savePart(@NotEmpty("json") String json) {
+        return save(json);
+    }
+
+    @Override
+    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        // TODO update related Panel and PanelInstance (old and new)
+        Kanban kanban = kanbanDao.parse(json);
+        panelDao.checkExist(kanban.getPanelCode());
+        return kanbanDao.update(kanban);
+    }
+
+    @Override
+    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        return update(json);
+    }
+
+    @Override
+    public int deleteAll() throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.deleteAll();
+    }
+
+    @Override
+    public int deleteOne(@NotEmpty("code") String code) throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.deleteOne(code);
+    }
+
+    @Override
+    public int delete(@NotEmpty("codes") List<String> codes) throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.delete(codes);
+    }
+
+    @Override
+    public List<Kanban> getByPanelCode(@NotEmpty("panelCode") String panelCode) {
+        panelDao.checkExist(panelCode);
+        return kanbanDao.findListBy("panelCode", Collections.singletonList(panelCode));
+    }
+
+    @Override
+    public List<String> getEnabledCodesByPanelCode(@NotEmpty("panelCode") String panelCode) {
+        panelDao.checkExist(panelCode);
+        Query<Kanban> query = kanbanDao.createQuery();
+        query.field("panelCode").equal(panelCode);
+        try {
+            return kanbanDao.findField(query, "code", String.class);
+        } catch (NoSuchFieldException e) {
+            throw new IllegalStateException(e.getMessage(), e);
+        }
+    }
+
+}

+ 331 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/PanelInstanceServiceImpl.java

@@ -0,0 +1,331 @@
+package com.uas.kanban.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.*;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.*;
+import com.uas.kanban.service.KanbanService;
+import com.uas.kanban.service.PanelInstanceService;
+import com.uas.kanban.service.ParameterService;
+import com.uas.kanban.support.DataSourceManager;
+import com.uas.kanban.support.KanbanParser;
+import com.uas.kanban.util.CollectionUtils;
+import com.uas.kanban.util.ObjectUtils;
+import com.uas.kanban.util.StringUtils;
+import me.chyxion.jdbc.NewbieJdbcSupport;
+import org.dom4j.DocumentException;
+import org.mongodb.morphia.query.Query;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.xml.transform.TransformerException;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * 面板实例
+ *
+ * @author sunyj
+ * @since 2017/10/18 16:33
+ */
+@Service
+public class PanelInstanceServiceImpl extends BaseService<PanelInstance> implements PanelInstanceService {
+
+    @Autowired
+    private PanelInstanceDao panelInstanceDao;
+
+    @Autowired
+    private PanelDao panelDao;
+
+    @Autowired
+    private KanbanDao kanbanDao;
+
+    @Autowired
+    private ParameterDao parameterDao;
+
+    @Autowired
+    private DataSourceDao dataSourceDao;
+
+    @Autowired
+    private KanbanService kanbanService;
+
+    @Autowired
+    private ParameterService parameterService;
+
+    @Autowired
+    private DataSourceManager dataSourceManager;
+
+    @Autowired
+    private KanbanParser kanbanParser;
+
+    @Override
+    public PanelInstance save(@NotEmpty("json") String json) {
+        PanelInstance panelInstance = panelInstanceDao.parse(json);
+        checkParameters(panelInstance);
+        initSwitchFrequence(panelInstance);
+        return panelInstanceDao.save(panelInstance);
+    }
+
+    /**
+     * 检查参数是否正确填写(没有遗漏参数、每个参数都有值并且类型匹配)
+     *
+     * @param panelInstance 面板实例
+     */
+    private void checkParameters(@NotEmpty("panelInstance") PanelInstance panelInstance) {
+        List<Parameter> parameters = panelInstance.fromParameters();
+        try {
+            if (!CollectionUtils.isEmpty(parameters)) {
+                for (Parameter parameter : parameters) {
+                    parameter.mayInitValue();
+                }
+            }
+            compare(parameters, generateParameters(panelInstance));
+        } catch (ClassNotFoundException | IOException | InstantiationException | IllegalAccessException e) {
+            throw new IllegalStateException("深克隆对象时失败", e);
+        }
+    }
+
+    /**
+     * 根据关联的面板,为面板实例生成参数
+     *
+     * @param panelInstance 面板实例
+     * @return 生成的参数
+     */
+    private List<Parameter> generateParameters(@NotEmpty("panelInstance") PanelInstance panelInstance) {
+        String panelCode = panelInstance.getPanelCode();
+        Panel panel = panelDao.checkExist(panelCode);
+        String dataSourceCode = panel.getDataSourceCode();
+        List<Parameter> parameters = parameterService.getByPanelCode(panelCode);
+        if (CollectionUtils.isEmpty(parameters)) {
+            return null;
+        }
+        for (Parameter parameter : parameters) {
+            // 如果是 SQL 类型,需要解析参数
+            if (parameter.getType() == Type.SQL) {
+                try {
+                    NewbieJdbcSupport jdbc = dataSourceManager.getJdbc(dataSourceCode);
+                    List<Map<String, Object>> listMap = jdbc.listMap(String.valueOf(parameter.getValue()));
+                    if (listMap == null) {
+                        listMap = new ArrayList<>();
+                    }
+                    parameter.setValue(JSONObject.toJSONString(listMap));
+                } catch (SQLException e) {
+                    throw new IllegalStateException("参数解析错误", e);
+                }
+            }
+        }
+        return parameters;
+    }
+
+    /**
+     * 比较参数数量、名称等是否一致
+     *
+     * @param parameters 比较的对象
+     * @param references 参照对象
+     */
+    private void compare(List<Parameter> parameters, List<Parameter> references)
+            throws NotSerializableException, InstantiationException, IllegalAccessException, ClassNotFoundException,
+            IOException {
+        // 因为对象会被修改,所以先进行深克隆,参照对象不需要克隆
+        List<Parameter> parametersClone = (List<Parameter>) ObjectUtils.clone(parameters);
+        int parametersSize = parametersClone == null ? 0 : parametersClone.size() - 1;
+        int referencesSize = references == null ? 0 : references.size() - 1;
+        for (int i = parametersSize; i >= 0; i--) {
+            Parameter parameter = parametersClone.get(i);
+            // 先检查值
+            parameter.checkValue();
+            // 再去除 code 相同的参数,以找出不一样的参数
+            for (int j = referencesSize; j >= 0; j--) {
+                Parameter reference = references.get(j);
+                if (Objects.equals(parameter.getCode(), reference.getCode())) {
+                    // 排除值的干扰
+                    reference.setValue(parameter.getValue());
+                    // 与参照对象不一致,说明填写的参数不规范
+                    if (!parameter.equals(reference)) {
+                        throw new IllegalArgumentException("参数属性不一致:" + parameter + ",应为:" + reference);
+                    }
+                    // 移除比较对象中通过检查的参数,最终剩下的全是不合法的参数
+                    parametersClone.remove(parameter);
+                    references.remove(reference);
+                }
+            }
+        }
+        if (!CollectionUtils.isEmpty(parametersClone)) {
+            throw new IllegalArgumentException("不需要填写参数:" + parametersClone);
+        }
+        if (!CollectionUtils.isEmpty(references)) {
+            throw new IllegalArgumentException("未填写参数:" + references);
+        }
+    }
+
+    /**
+     * 初始化切换频率
+     *
+     * @param panelInstance 面板实例
+     */
+    private void initSwitchFrequence(@NotEmpty("panelInstance") PanelInstance panelInstance)
+            throws IllegalArgumentException {
+        Panel panel = panelDao.checkExist(panelInstance.getPanelCode());
+        switch (panel.getDisplay()) {
+            case AutoSwitch:
+                if (panelInstance.getSwitchFrequency() == null) {
+                    panelInstance.setSwitchFrequency(PanelInstance.DEFAULT_SWITCH_FREQUENCY);
+                }
+                break;
+            case SplitScreen:
+                if (panelInstance.getSwitchFrequency() != null) {
+                    panelInstance.setSwitchFrequency(null);
+                }
+                break;
+        }
+    }
+
+    @Override
+    public PanelInstance savePart(@NotEmpty("json") String json) {
+        return save(json);
+    }
+
+    @Override
+    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        PanelInstance panelInstance = panelInstanceDao.parse(json);
+        checkParameters(panelInstance);
+        return panelInstanceDao.update(panelInstance);
+    }
+
+    @Override
+    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        return update(json);
+    }
+
+    @Override
+    public int deleteAll() throws OperationException {
+        // 需要检查实例是否是自动生成的,难以检查
+        throw new OperationException("不支持的操作");
+    }
+
+    @Override
+    public int deleteOne(@NotEmpty("code") String code) throws OperationException {
+        PanelInstance panelInstance = panelInstanceDao.findOne(code);
+        if (panelInstance != null) {
+            Boolean autoGenerated = panelInstance.getAutoGenerated();
+            if (autoGenerated != null && autoGenerated) {
+                throw new OperationException("不可删除该实例");
+            }
+        }
+        return super.deleteOne(code);
+    }
+
+    @Override
+    public int delete(@NotEmpty("codes") List<String> codes) throws OperationException {
+        List<PanelInstance> panelInstances = panelInstanceDao.findIn(codes);
+        if (CollectionUtils.isEmpty(panelInstances)) {
+            return 0;
+        }
+        for (PanelInstance panelInstance : panelInstances) {
+            Boolean autoGenerated = panelInstance.getAutoGenerated();
+            if (autoGenerated != null && autoGenerated) {
+                throw new OperationException("不可删除实例:" + panelInstance.getCode());
+            }
+        }
+        return super.delete(codes);
+    }
+
+    @Override
+    public Map<String, Object> parseData(@NotEmpty("code") String code, String kanbanCode) {
+        PanelInstance panelInstance = panelInstanceDao.checkExist(code);
+        Panel panel = panelDao.checkExist(panelInstance.getPanelCode());
+        List<String> enabledKanbanCodes = kanbanService.getEnabledCodesByPanelCode(panel.getCode());
+        // 如果指定了看板,则解析该看板,否则解析该面板下的第一个看板
+        if (!StringUtils.isEmpty(kanbanCode)) {
+            if (!enabledKanbanCodes.contains(kanbanCode)) {
+                throw new IllegalArgumentException("看板不存在或未启用:" + kanbanCode);
+            }
+        } else {
+            kanbanCode = enabledKanbanCodes.get(0);
+        }
+        Kanban kanban = kanbanDao.checkExist(kanbanCode);
+        String content = kanban.getContent();
+        String title = kanban.getTitle();
+        if (StringUtils.isEmpty(content)) {
+            throw new IllegalStateException("看板内容为空:" + kanbanCode);
+        }
+        List<Parameter> parameters = panelInstance.fromParameters();
+        // 解析看板
+        String kanbanContent;
+        try {
+            NewbieJdbcSupport jdbc = dataSourceManager.getJdbc(panel.getDataSourceCode());
+            kanbanContent = kanbanParser.parseXml(content, title, parameters, jdbc);
+        } catch (DocumentException e) {
+            throw new IllegalStateException("xml 解析出错", e);
+        } catch (TransformerException e) {
+            throw new IllegalStateException("xml 映射出错", e);
+        } catch (IOException e) {
+            throw new IllegalStateException("xml 转换出错", e);
+        } catch (SQLException e) {
+            throw new IllegalStateException("数据库连接错误", e);
+        }
+        Map<String, Object> result = new HashMap<>();
+        Map<String, Object> instance = new HashMap<>();
+        instance.put("parameters", parameters);
+        instance.put("kanbanCodes", enabledKanbanCodes);
+        instance.put("display", panel.getDisplay());
+        instance.put("switchFrequency", panelInstance.getSwitchFrequency());
+        instance.put("refreshFrequency", panelInstance.getRefreshFrequency());
+        result.put("instance", instance);
+        List<JSONObject> data = new ArrayList<>();
+        data.add(JSONObject.parseObject(kanbanContent));
+        result.put("data", data);
+        // TODO 多个看板返回的json
+        return result;
+    }
+
+    @Override
+    public int deleteByPanelCodes(@NotEmpty("panelCodes") List<String> panelCodes) {
+        Query<PanelInstance> query = panelInstanceDao.createQuery().field("panelCode").in(panelCodes);
+        return panelInstanceDao.delete(query);
+    }
+
+    @Override
+    public List<PanelInstance> getByPanelCode(@NotEmpty("panelCode") String panelCode) {
+        panelDao.checkExist(panelCode);
+        return panelInstanceDao.findListBy("panelCode", Collections.singletonList(panelCode));
+    }
+
+    @Override
+    public List<PanelInstance> generateInstances(@NotEmpty("userCodes") List<String> userCodes, @NotEmpty("panelCodes") List<String> panelCodes) {
+        List<PanelInstance> panelInstances = new ArrayList<>();
+        for (String panelCode : panelCodes) {
+            Panel panel = panelDao.checkExist(panelCode);
+            PanelInstance panelInstance = new PanelInstance();
+            panelInstance.setName(panel.getName());
+            panelInstance.setIconCls(panel.getIconCls());
+            panelInstance.setAutoGenerated(true);
+            panelInstance.setPanelCode(panelCode);
+            initSwitchFrequence(panelInstance);
+            panelInstance.toParameters(generateParameters(panelInstance));
+            for (String userCode : userCodes) {
+                // TODO generateInstances
+                // TODO implement batch save
+                try {
+                    PanelInstance panelInstanceClone = ObjectUtils.clone(panelInstance);
+                    panelInstanceClone.setUserCode(userCode);
+                    panelInstances.add(panelInstanceDao.save(panelInstanceClone));
+                } catch (ClassNotFoundException | IOException e) {
+                    throw new IllegalStateException("深克隆对象时失败", e);
+                }
+            }
+        }
+        return panelInstances;
+    }
+
+    @Override
+    public PanelInstance saveToDesktop(@NotEmpty("json") String json) {
+        // TODO save to desktop
+        return null;
+    }
+
+}

+ 88 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/PanelServiceImpl.java

@@ -0,0 +1,88 @@
+package com.uas.kanban.service.impl;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.DataSourceDao;
+import com.uas.kanban.dao.PanelDao;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.Panel;
+import com.uas.kanban.model.PanelInstance;
+import com.uas.kanban.model.User;
+import com.uas.kanban.service.PanelInstanceService;
+import com.uas.kanban.service.PanelService;
+import com.uas.kanban.support.SystemSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 面板
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:20
+ */
+@Service
+public class PanelServiceImpl extends BaseService<Panel> implements PanelService {
+
+    @Autowired
+    private PanelDao panelDao;
+
+    @Autowired
+    private DataSourceDao dataSourceDao;
+
+    @Autowired
+    private PanelInstanceService panelInstanceService;
+
+    @Override
+    public Panel save(@NotEmpty("json") String json) {
+        // TODO generate PanelInstance
+        Panel panel = panelDao.parse(json);
+        dataSourceDao.checkExist(panel.getDataSourceCode());
+        User user = SystemSession.checkUser();
+        assignPanel(user.getCode(), Collections.singletonList(panel.getCode()));
+        return panelDao.save(panel);
+    }
+
+    @Override
+    public Panel savePart(@NotEmpty("json") String json) {
+        return save(json);
+    }
+
+    @Override
+    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        // TODO update related PanelInstance
+        Panel panel = panelDao.parse(json);
+        dataSourceDao.checkExist(panel.getDataSourceCode());
+        return panelDao.update(panel);
+    }
+
+    @Override
+    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        return update(json);
+    }
+
+    @Override
+    public int deleteAll() throws OperationException {
+        // TODO delete related PanelInstance
+        return super.deleteAll();
+    }
+
+    @Override
+    public int deleteOne(@NotEmpty("code") String code) throws OperationException {
+        // TODO delete related PanelInstance
+        return super.deleteOne(code);
+    }
+
+    @Override
+    public int delete(@NotEmpty("codes") List<String> codes) throws OperationException {
+        // TODO delete related PanelInstance
+        return super.delete(codes);
+    }
+
+    @Override
+    public List<PanelInstance> assignPanel(@NotEmpty("userCode") String userCode, @NotEmpty("panelCodes") List<String> panelCodes) {
+        return panelInstanceService.generateInstances(Collections.singletonList(userCode), panelCodes);
+    }
+}

+ 116 - 0
kanban-console/src/main/java/com/uas/kanban/service/impl/ParameterServiceImpl.java

@@ -0,0 +1,116 @@
+package com.uas.kanban.service.impl;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.base.BaseService;
+import com.uas.kanban.dao.PanelDao;
+import com.uas.kanban.dao.ParameterDao;
+import com.uas.kanban.exception.OperationException;
+import com.uas.kanban.model.Parameter;
+import com.uas.kanban.service.ParameterService;
+import com.uas.kanban.util.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 参数
+ *
+ * @author sunyj
+ * @since 2017/10/18 14:25
+ */
+@Service
+public class ParameterServiceImpl extends BaseService<Parameter> implements ParameterService {
+
+    @Autowired
+    private ParameterDao parameterDao;
+
+    @Autowired
+    private PanelDao panelDao;
+
+    @Override
+    public Parameter save(@NotEmpty("json") String json) {
+        // TODO update related Panel and PanelInstance
+        Parameter parameter = parameterDao.parse(json);
+        panelDao.checkExist(parameter.getPanelCode());
+        checkInputMode(parameter);
+        return parameterDao.save(parameter);
+    }
+
+    /**
+     * 检查输入方式
+     *
+     * @param parameter 参数
+     * @throws IllegalArgumentException 输入方式为 DropDownBox 时,未提供可选值或者默认值的序号不合法
+     */
+    private void checkInputMode(@NotEmpty("parameter") Parameter parameter) {
+        Parameter.InputMode inputMode = parameter.getInputMode();
+        // 输入方式为 {@link InputMode#Radio} 或 {@link InputMode#DropDownBox} 时,需提供可选值
+        if (inputMode != null) {
+            List<Object> optionalValues = parameter.getOptionalValues();
+            Integer defaultOptionalValueIndex = parameter.getDefaultOptionalValueIndex();
+            switch (inputMode) {
+                case DropDownBox:
+                    if (CollectionUtils.isEmpty(optionalValues)) {
+                        throw new IllegalArgumentException("输入方式为 DropDownBox 时,需提供可选值");
+                    }
+                    if (defaultOptionalValueIndex != null
+                            && (defaultOptionalValueIndex < 1 || defaultOptionalValueIndex > optionalValues.size())) {
+                        throw new IllegalArgumentException("默认值的序号不合法:" + defaultOptionalValueIndex);
+                    }
+                    break;
+                case Manual:
+                    if (!CollectionUtils.isEmpty(optionalValues) || defaultOptionalValueIndex != null) {
+                        throw new IllegalArgumentException(
+                                "输入方式为 Manual 时,不可指定 optionalValues 和 defaultOptionalValueIndex");
+                    }
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public Parameter savePart(@NotEmpty("json") String json) {
+        return save(json);
+    }
+
+    @Override
+    public int update(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        // TODO update related Panel and PanelInstance (old and new)
+        Parameter parameter = parameterDao.parse(json);
+        panelDao.checkExist(parameter.getPanelCode());
+        checkInputMode(parameter);
+        return parameterDao.update(parameter);
+    }
+
+    @Override
+    public int updatePart(@NotEmpty("json") String json) throws IllegalArgumentException, OperationException {
+        return update(json);
+    }
+
+    @Override
+    public int deleteAll() throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.deleteAll();
+    }
+
+    @Override
+    public int deleteOne(@NotEmpty("code") String code) throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.deleteOne(code);
+    }
+
+    @Override
+    public int delete(@NotEmpty("codes") List<String> codes) throws OperationException {
+        // TODO update related Panel and PanelInstance
+        return super.delete(codes);
+    }
+
+    @Override
+    public List<Parameter> getByPanelCode(@NotEmpty("panelCode") String panelCode) {
+        panelDao.checkExist(panelCode);
+        return parameterDao.findListBy("panelCode", Collections.singletonList(panelCode));
+    }
+
+}

+ 1 - 4
kanban-console/src/main/java/com/uas/kanban/support/DataSourceManager.java

@@ -48,10 +48,7 @@ public class DataSourceManager {
      */
     private DataSourceCache getCache(@NotEmpty("dataSourceCode") String dataSourceCode) throws SQLException {
         DataSourceCache dataSourceCache = dataSourceCaches.get(dataSourceCode);
-        DataSource dataSource = dataSourceDao.findOne(dataSourceCode);
-        if (dataSource == null) {
-            throw new IllegalStateException("数据源不存在:" + dataSourceCode);
-        }
+        DataSource dataSource = dataSourceDao.checkExist(dataSourceCode);
         dataSourceDao.checkFields(dataSource);
         // 存在数据库连接缓存
         if (dataSourceCache != null) {

+ 529 - 527
kanban-console/src/main/java/com/uas/kanban/support/TemplateParser.java → kanban-console/src/main/java/com/uas/kanban/support/KanbanParser.java

@@ -1,527 +1,529 @@
-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 替换后的模版内容
-     */
-    private String replaceParameters(@NotEmpty("content") String content, List<GlobalParameter> globalParameters,
-                                     @NotEmpty("needSingleQuote") Boolean needSingleQuote, @NotEmpty("jdbc") NewbieJdbcSupport jdbc) throws SQLException {
-        if (CollectionUtils.isEmpty(globalParameters)) {
-            return content;
-        }
-        Map<String, GlobalParameter> 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<String, Object> 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) {
-                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 数据
-     */
-    public String parseXml(@NotEmpty("content") String content, String title, List<GlobalParameter> 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 处理后的模版内容
-     */
-    private 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(">", "&gt;").replaceAll("<", "&lt;");
-            content = content.substring(0, start) + sql + content.substring(end);
-            matcher = pattern.matcher(content);
-        }
-        return content;
-    }
-
-    /**
-     * 处理模版中的 form 组件
-     *
-     * @param content          模版内容
-     * @param jdbc             NewbieJdbcSupport 对象
-     * @param globalParameters 公共参数
-     * @return 解析后的模版内容
-     */
-    @SuppressWarnings("unchecked")
-    private String processForm(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<GlobalParameter> globalParameters)
-            throws DocumentException, IllegalStateException, SQLException {
-        Document document = getDocument(content);
-        // 获取 form 组件
-        List<Element> 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<Element> fieldElements = element.elements("field");
-            if (CollectionUtils.isEmpty(fieldElements)) {
-                continue;
-            }
-            checkCount(jdbc.getDataSource().getConnection(), sql);
-            Map<String, Object> 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 解析后的模版内容
-     */
-    @SuppressWarnings("unchecked")
-    private String processGrid(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<GlobalParameter> globalParameters)
-            throws DocumentException, IllegalStateException, SQLException {
-        Document document = getDocument(content);
-        // 获取 form 组件
-        List<Element> 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<Element> fieldElements = element.elements("field");
-            if (CollectionUtils.isEmpty(fieldElements)) {
-                continue;
-            }
-            // 提取 field 名称
-            List<String> 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<Map<String, Object>> listMap = jdbc.listMap(sql);
-            if (CollectionUtils.isEmpty(listMap)) {
-                continue;
-            }
-            for (Map<String, Object> 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 解析后的模版内容
-     */
-    @SuppressWarnings("unchecked")
-    private String processBarAndLine(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<GlobalParameter> globalParameters)
-            throws DocumentException, IllegalStateException, SQLException {
-        Document document = getDocument(content);
-        // 获取 bar 和 line 组件
-        List<Element> 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<String, List<Object>> map = convert(jdbc.listMap(sql));
-            if (CollectionUtils.isEmpty(map)) {
-                continue;
-            }
-
-            // xvalue 字段名
-            String xvalue = element.attribute("xvalue").getText();
-            if (xvalue != null) {
-                // 设置 xvalue
-                element.attribute("xfields").setText(toString(map.remove(xvalue.toUpperCase())));
-            }
-
-            // 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<Object> 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 解析后的模版内容
-     */
-    @SuppressWarnings("unchecked")
-    private String processPie(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<GlobalParameter> globalParameters)
-            throws DocumentException, IllegalStateException, SQLException {
-        Document document = getDocument(content);
-        // 获取 bar 和 line 组件
-        List<Element> 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<Map<String, Object>> 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<String, Object> 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 对象
-     */
-    private Document getDocument(@NotEmpty("xml") String xml) throws DocumentException {
-        return new SAXReader().read(new StringReader(xml));
-    }
-
-    /**
-     * 检查当前条件下的结果数目是否超出限制
-     *
-     * @param connection 数据库连接
-     * @param sql        查询语句
-     */
-    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 结果数目
-     */
-    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();
-            return resultSet.getInt(1);
-        } 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<String, Object>> 转为 Map<String, List<Object>>
-     *
-     * @param listMap 将转换的对象,每个 Map 的键都相同,否则会得到非预期的结果<br/>
-     *                如下所示:
-     *                <p/>
-     *                <table border=1 cellpadding=5 cellspacing=0 summary=
-     *                "Key and value">
-     *                <tr>
-     *                <th>输入</th>
-     *                <th>输出</th>
-     *                <th>结果</th>
-     *                </tr>
-     *                <tr>
-     *                <td>[{age=12, name=Joe}, {age=31, name=Lee}]</td>
-     *                <td>{name=[Joe, Lee], age=[12, 31]}</td>
-     *                <td>预期</td>
-     *                </tr>
-     *                <tr>
-     *                <td>[{age=12, name=Joe}, {name=Lee, agee=31}]</td>
-     *                <td>{name=[Joe, Lee], age=[12], agee=[31]}</td>
-     *                <td>非预期</td>
-     *                </tr>
-     *                </table>
-     * @return 转换的结果
-     */
-    private Map<String, List<Object>> convert(List<Map<String, Object>> listMap) {
-        Map<String, List<Object>> result = new HashMap<>();
-        if (CollectionUtils.isEmpty(listMap)) {
-            return result;
-        }
-        for (Map<String, Object> map : listMap) {
-            Set<Entry<String, Object>> entrySet = map.entrySet();
-            for (Entry<String, Object> entry : entrySet) {
-                String key = entry.getKey();
-                Object value = entry.getValue();
-                List<Object> 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 处理后的模版内容
-     */
-    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 处理后的模版内容
-     */
-    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();
-    }
-}
+package com.uas.kanban.support;
+
+import com.uas.kanban.annotation.NotEmpty;
+import com.uas.kanban.model.Parameter;
+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 KanbanParser {
+
+    /**
+     * 查询条件的结果数目限制
+     */
+    private static final int MAX_RECORD_SIZE = 10000;
+
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * 替换看板中的参数为实际值
+     *
+     * @param content         看板内容
+     * @param parameters      参数
+     * @param needSingleQuote 参数值是否以单引号括起来(用于 sql)
+     * @param jdbc            NewbieJdbcSupport 对象
+     * @return 替换后的看板内容
+     */
+    private String replaceParameters(@NotEmpty("content") String content, List<Parameter> parameters,
+                                     @NotEmpty("needSingleQuote") Boolean needSingleQuote, @NotEmpty("jdbc") NewbieJdbcSupport jdbc) throws SQLException {
+        if (CollectionUtils.isEmpty(parameters)) {
+            return content;
+        }
+        Map<String, Parameter> map = new HashMap<>();
+        for (Parameter parameter : parameters) {
+            map.put(parameter.getName(), parameter);
+        }
+        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 parameterName = matcher.group(1);
+            Parameter parameter = map.get(parameterName);
+            if (parameter == null) {
+                throw new IllegalStateException("未启用参数:" + parameterName);
+            }
+            Object value = parameter.getValue();
+            if (value == null) {
+                throw new IllegalStateException("参数值为null:" + parameterName);
+            }
+
+            // 如果参数值为 {@link Type#SQL} ,需要取得查询结果
+            if (parameter.getType() == Type.SQL) {
+                String stringValue = (String) value;
+                // 不递归替换参数
+                // stringValue=replaceParameters(stringValue,parameters,true,jdbc);
+                checkCount(jdbc.getDataSource().getConnection(), stringValue);
+                Map<String, Object> data = jdbc.findMap(stringValue);
+                if (CollectionUtils.isEmpty(data)) {
+                    throw new IllegalStateException("参数通过 SQL 取得结果为空:" + parameterName);
+                } else if (data.size() > 1) {
+                    throw new IllegalStateException("参数通过 SQL 取得结果不止一列:" + parameterName);
+                }
+                value = data.values().iterator().next();
+            }
+            // TODO value 为时间
+            if (needSingleQuote != null && needSingleQuote) {
+                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 parameters 参数
+     * @param jdbc       NewbieJdbcSupport 对象
+     * @return 解析后的 json 数据
+     */
+    public String parseXml(@NotEmpty("content") String content, String title, List<Parameter> parameters, @NotEmpty("jdbc") NewbieJdbcSupport jdbc)
+            throws DocumentException, TransformerException, IOException, IllegalStateException, SQLException {
+        content = processSql(content);
+        // 替换标题中的参数
+        if (!com.uas.kanban.util.StringUtils.isEmpty(title)) {
+            title = replaceParameters(title, parameters, false, jdbc);
+        }
+        content = processForm(content, jdbc, parameters);
+        content = processGrid(content, jdbc, parameters);
+        content = processBarAndLine(content, jdbc, parameters);
+        content = processPie(content, jdbc, parameters);
+        content = finalProcess(content, title);
+        return TranslateHelper.xmlToJson(content);
+    }
+
+    /**
+     * 处理 sql ,包括替换特殊字符等
+     *
+     * @param content 看板内容
+     * @return 处理后的看板内容
+     */
+    private 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(">", "&gt;").replaceAll("<", "&lt;");
+            content = content.substring(0, start) + sql + content.substring(end);
+            matcher = pattern.matcher(content);
+        }
+        return content;
+    }
+
+    /**
+     * 处理看板中的 form 组件
+     *
+     * @param content    看板内容
+     * @param jdbc       NewbieJdbcSupport 对象
+     * @param parameters 参数
+     * @return 解析后的看板内容
+     */
+    @SuppressWarnings("unchecked")
+    private String processForm(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<Parameter> parameters)
+            throws DocumentException, IllegalStateException, SQLException {
+        Document document = getDocument(content);
+        // 获取 form 组件
+        List<Element> elements = document.selectNodes("//form");
+        for (Element element : elements) {
+            // 执行 sql ,获取数据
+            String sql = element.attribute("sql").getText();
+            if (StringUtils.isEmpty(sql)) {
+                continue;
+            }
+            sql = replaceParameters(sql, parameters, true, jdbc);
+            // 获取 field 节点
+            List<Element> fieldElements = element.elements("field");
+            if (CollectionUtils.isEmpty(fieldElements)) {
+                continue;
+            }
+            checkCount(jdbc.getDataSource().getConnection(), sql);
+            Map<String, Object> 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 parameters 参数
+     * @return 解析后的看板内容
+     */
+    @SuppressWarnings("unchecked")
+    private String processGrid(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<Parameter> parameters)
+            throws DocumentException, IllegalStateException, SQLException {
+        Document document = getDocument(content);
+        // 获取 form 组件
+        List<Element> elements = document.selectNodes("//grid");
+        for (Element element : elements) {
+            // 执行 sql ,获取数据
+            String sql = element.attribute("sql").getText();
+            if (StringUtils.isEmpty(sql)) {
+                continue;
+            }
+            sql = replaceParameters(sql, parameters, true, jdbc);
+            // 获取 field 节点
+            List<Element> fieldElements = element.elements("field");
+            if (CollectionUtils.isEmpty(fieldElements)) {
+                continue;
+            }
+            // 提取 field 名称
+            List<String> 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<Map<String, Object>> listMap = jdbc.listMap(sql);
+            if (CollectionUtils.isEmpty(listMap)) {
+                continue;
+            }
+            for (Map<String, Object> 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 parameters 参数
+     * @return 解析后的看板内容
+     */
+    @SuppressWarnings("unchecked")
+    private String processBarAndLine(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<Parameter> parameters)
+            throws DocumentException, IllegalStateException, SQLException {
+        Document document = getDocument(content);
+        // 获取 bar 和 line 组件
+        List<Element> elements = document.selectNodes("//bar | //line");
+        for (Element element : elements) {
+            // 执行 sql ,获取数据
+            String sql = element.attribute("sql").getText();
+            if (StringUtils.isEmpty(sql)) {
+                continue;
+            }
+            sql = replaceParameters(sql, parameters, true, jdbc);
+            checkCount(jdbc.getDataSource().getConnection(), sql);
+            Map<String, List<Object>> map = convert(jdbc.listMap(sql));
+            if (CollectionUtils.isEmpty(map)) {
+                continue;
+            }
+
+            // xvalue 字段名
+            String xvalue = element.attribute("xvalue").getText();
+            if (xvalue != null) {
+                // 设置 xvalue
+                element.attribute("xfields").setText(toString(map.remove(xvalue.toUpperCase())));
+            }
+
+            // 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<Object> 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 parameters 参数
+     * @return 解析后的看板内容
+     */
+    @SuppressWarnings("unchecked")
+    private String processPie(@NotEmpty("content") String content, @NotEmpty("jdbc") NewbieJdbcSupport jdbc, List<Parameter> parameters)
+            throws DocumentException, IllegalStateException, SQLException {
+        Document document = getDocument(content);
+        // 获取 bar 和 line 组件
+        List<Element> elements = document.selectNodes("//pie");
+        for (Element element : elements) {
+            // 执行 sql ,获取数据
+            String sql = element.attribute("sql").getText();
+            if (StringUtils.isEmpty(sql)) {
+                continue;
+            }
+            sql = replaceParameters(sql, parameters, true, jdbc);
+            checkCount(jdbc.getDataSource().getConnection(), sql);
+            List<Map<String, Object>> 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<String, Object> 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 对象
+     */
+    private Document getDocument(@NotEmpty("xml") String xml) throws DocumentException {
+        return new SAXReader().read(new StringReader(xml));
+    }
+
+    /**
+     * 检查当前条件下的结果数目是否超出限制
+     *
+     * @param connection 数据库连接
+     * @param sql        查询语句
+     */
+    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 结果数目
+     */
+    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();
+            return resultSet.getInt(1);
+        } 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<String, Object>> 转为 Map<String, List<Object>>
+     *
+     * @param listMap 将转换的对象,每个 Map 的键都相同,否则会得到非预期的结果<br/>
+     *                如下所示:
+     *                <p/>
+     *                <table border=1 cellpadding=5 cellspacing=0 summary=
+     *                "Key and value">
+     *                <tr>
+     *                <th>输入</th>
+     *                <th>输出</th>
+     *                <th>结果</th>
+     *                </tr>
+     *                <tr>
+     *                <td>[{age=12, name=Joe}, {age=31, name=Lee}]</td>
+     *                <td>{name=[Joe, Lee], age=[12, 31]}</td>
+     *                <td>预期</td>
+     *                </tr>
+     *                <tr>
+     *                <td>[{age=12, name=Joe}, {name=Lee, agee=31}]</td>
+     *                <td>{name=[Joe, Lee], age=[12], agee=[31]}</td>
+     *                <td>非预期</td>
+     *                </tr>
+     *                </table>
+     * @return 转换的结果
+     */
+    private Map<String, List<Object>> convert(List<Map<String, Object>> listMap) {
+        Map<String, List<Object>> result = new HashMap<>();
+        if (CollectionUtils.isEmpty(listMap)) {
+            return result;
+        }
+        for (Map<String, Object> map : listMap) {
+            Set<Entry<String, Object>> entrySet = map.entrySet();
+            for (Entry<String, Object> entry : entrySet) {
+                String key = entry.getKey();
+                Object value = entry.getValue();
+                List<Object> 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 处理后的看板内容
+     */
+    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 处理后的看板内容
+     */
+    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();
+    }
+}