zlSun 1 year ago
parent
commit
e65ad92cd7
100 changed files with 12893 additions and 0 deletions
  1. 34 0
      .gitignore
  2. 165 0
      LICENSE
  3. 21 0
      RELEASE.md
  4. 39 0
      Roadmap.txt
  5. 115 0
      application.properties
  6. 88 0
      datagear-analysis/pom.xml
  7. 61 0
      datagear-analysis/src/main/java/org/datagear/analysis/AbstractDataNameType.java
  8. 76 0
      datagear-analysis/src/main/java/org/datagear/analysis/AbstractIdentifiable.java
  9. 100 0
      datagear-analysis/src/main/java/org/datagear/analysis/Category.java
  10. 67 0
      datagear-analysis/src/main/java/org/datagear/analysis/Chart.java
  11. 174 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartDataSet.java
  12. 250 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartDefinition.java
  13. 109 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartParam.java
  14. 153 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartPlugin.java
  15. 63 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartPluginManager.java
  16. 92 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartQuery.java
  17. 51 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartResult.java
  18. 40 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartResultError.java
  19. 207 0
      datagear-analysis/src/main/java/org/datagear/analysis/ChartTheme.java
  20. 160 0
      datagear-analysis/src/main/java/org/datagear/analysis/Dashboard.java
  21. 87 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardQuery.java
  22. 91 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardQueryHandler.java
  23. 67 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardResult.java
  24. 45 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardTheme.java
  25. 32 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardThemeSource.java
  26. 31 0
      datagear-analysis/src/main/java/org/datagear/analysis/DashboardWidget.java
  27. 31 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataNameType.java
  28. 90 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSet.java
  29. 39 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSetException.java
  30. 158 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSetParam.java
  31. 150 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSetProperty.java
  32. 236 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSetQuery.java
  33. 51 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSetResult.java
  34. 102 0
      datagear-analysis/src/main/java/org/datagear/analysis/DataSign.java
  35. 43 0
      datagear-analysis/src/main/java/org/datagear/analysis/Icon.java
  36. 24 0
      datagear-analysis/src/main/java/org/datagear/analysis/Identifiable.java
  37. 65 0
      datagear-analysis/src/main/java/org/datagear/analysis/RenderContext.java
  38. 39 0
      datagear-analysis/src/main/java/org/datagear/analysis/RenderException.java
  39. 31 0
      datagear-analysis/src/main/java/org/datagear/analysis/ResolvableDataSet.java
  40. 54 0
      datagear-analysis/src/main/java/org/datagear/analysis/ResolvedDataSetResult.java
  41. 166 0
      datagear-analysis/src/main/java/org/datagear/analysis/ResultDataFormat.java
  42. 31 0
      datagear-analysis/src/main/java/org/datagear/analysis/ResultDataFormatAware.java
  43. 48 0
      datagear-analysis/src/main/java/org/datagear/analysis/SimpleDashboardQueryHandler.java
  44. 58 0
      datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboard.java
  45. 200 0
      datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboardWidget.java
  46. 193 0
      datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboardWidgetResManager.java
  47. 140 0
      datagear-analysis/src/main/java/org/datagear/analysis/Theme.java
  48. 74 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/AbstractValueConstraint.java
  49. 19 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/Constraint.java
  50. 27 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/Max.java
  51. 27 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/MaxLength.java
  52. 27 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/Min.java
  53. 27 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/MinLength.java
  54. 27 0
      datagear-analysis/src/main/java/org/datagear/analysis/constraint/Required.java
  55. 200 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractChartPlugin.java
  56. 286 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractChartPluginManager.java
  57. 395 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractCsvDataSet.java
  58. 77 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractCsvFileDataSet.java
  59. 573 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java
  60. 770 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java
  61. 387 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractJsonDataSet.java
  62. 77 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractJsonFileDataSet.java
  63. 145 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractResolvableDataSet.java
  64. 89 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractTemplateDashboardWidgetResManager.java
  65. 179 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/BytesIcon.java
  66. 146 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/CategorizationResolver.java
  67. 38 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ChartParamValueConverter.java
  68. 90 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ChartResultErrorMessage.java
  69. 140 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ChartWidget.java
  70. 26 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ChartWidgetSource.java
  71. 120 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ConcurrentChartPluginManager.java
  72. 83 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/CsvDirectoryFileDataSet.java
  73. 85 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/CsvValueDataSet.java
  74. 28 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataFormat.java
  75. 283 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetFmkTemplateResolver.java
  76. 122 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetParamValueConverter.java
  77. 41 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetParamValueRequiredException.java
  78. 58 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetPropertyNotFoundException.java
  79. 258 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetPropertyValueConverter.java
  80. 59 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetSourceParseException.java
  81. 272 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataValueConverter.java
  82. 72 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DataValueConvertionException.java
  83. 81 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/DefaultRenderContext.java
  84. 82 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ErrorMessageDashboardResult.java
  85. 89 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java
  86. 206 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/FileTemplateDashboardWidgetResManager.java
  87. 39 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/HeaderContentNotNameValueObjArrayJsonException.java
  88. 685 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/HttpDataSet.java
  89. 597 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/JsonChartPluginPropertiesResolver.java
  90. 84 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java
  91. 216 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/JsonSupport.java
  92. 73 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/JsonValueDataSet.java
  93. 79 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/LocationIcon.java
  94. 149 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/LocationResource.java
  95. 130 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/NameAsTemplateDashboardWidgetResManager.java
  96. 57 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/NotNameValueObjArrayJsonException.java
  97. 73 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ProfileDataSet.java
  98. 465 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java
  99. 55 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/ReadJsonDataPathException.java
  100. 39 0
      datagear-analysis/src/main/java/org/datagear/analysis/support/RequestContentNotNameValueObjArrayJsonException.java

+ 34 - 0
.gitignore

@@ -0,0 +1,34 @@
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+target/
+**/target/
+
+
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IntelliJ project files
+.idea
+*.iml
+out
+gen

+ 165 - 0
LICENSE

@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/]
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.

+ 21 - 0
RELEASE.md

@@ -0,0 +1,21 @@
+# DataGear
+
+## 版本发布流程
+
+1. 以当前分支新建版本标记,名称为:v[version],描述为:version [version];
+
+2. 切换到版本标记,执行maven构建命令:`mvn clean package` ;
+
+3. 将刚才新建的标记推送到仓库保存;
+
+4. 将构建的程序包(`datagear-web/target/datagear-[version]-packages/`目录内)发布到Gitee、官网;
+
+5. 将当前分支合并至主干;
+
+6. 以主干新建新版本开发分支,分支名为`dev-*`;
+
+7. 切换至新版本开发分支,修改`pom.xml`文件中的`version`标签内的版本号为下一个版本;
+
+8. 执行统一修改版本号的maven命令:`mvn -N versions:update-child-modules antrun:run` ;
+
+9. 提交并推送新版本开发分支。

+ 39 - 0
Roadmap.txt

@@ -0,0 +1,39 @@
+下一版本:
+	
+	ok 看板源码编辑模式支持折叠代码;
+	ok 可变模型数据集:数据集解除必须有属性的限制,DataSet添加isMutableModel()方法标识可变模型数据集,返回数据不受属性列表限制;
+	ok 重构图表主题结构,添加titleTheme、legendTheme属性,同时支持设置字体大小;
+	ok 图表支持库ECharts版本由5.2.2升级至5.3.1;
+	ok 共享看板支持设置密码;
+	ok 整理合并数据库脚本(以2.13为基础版,不支持更低版本自动升级);
+	ok 看板可视编辑:编辑元素属性(图片、超链接、视频、文本标签);
+	ok 看板可视编辑:新增插入文本标签功能;
+	ok 看板可视编辑:图表选项文本域改为格式化的文本编辑器;
+	ok 看板可视编辑:添加快捷执行按钮,点击可直接执行上次操作,不必重新在菜单中选择;
+	ok 看板可视编辑:图表列表面板可拖拽;
+	ok 看板可视编辑:刷新后保持元素边线状态;
+	ok 看板可视编辑:修复刷新后切换至源码模式未同步的BUG;
+	
+待定:
+	文件数据集缓存;
+	添加更多内置图表插件:主题河流图、地图热力图;
+	用户/角色登录后跳转首页管理功能,可定义指定用户/角色登录后的跳转首页;
+	看板可视编辑:插入表单;
+	图表插件在线定义功能;
+	看板图表导出数据功能;
+	看板模板管理功能;
+	看板模板、布局、主题素材库;
+	组合数据集:
+		新建组合数据集(CombineDataSet),选定多个其他数据集,通过SQL语句连接、选取它们,生成新的数据;
+	图表参数;
+	
+待研究:
+	提供服务API,使图表、看板可以方便整合至第三方前端代码中;
+	看板多端支持;
+	数据集支持更多数据库,比如:MongoDB、Redis;
+	多维分析图表类型;
+	数据填报;
+	应用级集成;
+	3D图表;
+	SQL跨库查询;
+	docker安装镜像;

+ 115 - 0
application.properties

@@ -0,0 +1,115 @@
+#--UTF-8 file--
+
+#工作空间主目录,可在系统环境变量中设置此项以修改工作空间主目录
+DataGearWorkspace=${user.home}/.datagear
+
+#重设密码创建校验文件的目录
+directory.resetPasswordCheckFile=${user.home}
+
+#驱动程序管理主目录
+directory.driver=${DataGearWorkspace}/driver
+
+#系统使用的derby数据库主目录
+directory.derby=${DataGearWorkspace}/derby
+
+#临时文件目录
+directory.temp=${DataGearWorkspace}/temp
+
+#图表插件主目录
+directory.chartPlugin=${DataGearWorkspace}/chartPlugin
+
+#看板主目录
+directory.dashboard=${DataGearWorkspace}/dashboard
+
+#看板全局资源主目录
+directory.dashboardGlobalRes=${DataGearWorkspace}/dashboardGlobalRes
+
+#看板模板内引用全局资源的URL前缀,主要用于标识看版内的全局资源
+#应不以'/'开头且以'/'结尾,留空表示不设前缀
+dashboardGlobalResUrlPrefix=global/
+
+#数据集文件主目录
+directory.dataSet=${DataGearWorkspace}/dataSet
+
+#数据编辑界面自定义URL构建器脚本文件
+schemaUrlBuilderScriptFile=${DataGearWorkspace}/db_url_builder.js
+
+#已载入过的图表插件上次修改时间信息存储文件
+builtinChartPluginLastModifiedFile=${DataGearWorkspace}/builtinChartPluginLastModified
+
+#是否禁用匿名用户功能,禁用后,匿名用户将不能使用系统功能
+#可选值:true 表示禁用;false 表示不禁用
+disableAnonymous=false
+
+#是否禁用注册功能
+#可选值:true 表示禁用;false 表示不禁用
+disableRegister=false
+
+#是否禁用检测新版本功能
+#可选值:true 表示禁用;false 表示不禁用
+disableDetectNewVersion=false
+
+#默认角色,可选值:ROLE_DATA_ADMIN、ROLE_DATA_ANALYST
+#ROLE_DATA_ADMIN 数据管理员,可以管理数据源、数据集、图表、看板
+#ROLE_DATA_ANALYST 数据分析员,仅可查看数据源、数据集、图表、看板,展示图表和看板
+#默认角色:注册用户
+defaultRole.register=ROLE_DATA_ADMIN
+#默认角色:管理员添加用户
+defaultRole.add=ROLE_DATA_ADMIN
+#默认角色:匿名用户
+defaultRole.anonymous=ROLE_DATA_ADMIN
+
+#清理临时目录
+#可删除的过期文件分钟数
+cleanTempDirectory.expiredMinutes=1440
+#执行清理时间间隔
+cleanTempDirectory.interval=0 0/10 * * * ?
+
+#数据库
+#datasource.driverClassName=org.apache.derby.jdbc.EmbeddedDriver
+#datasource.url=jdbc:derby:${directory.derby};create=true
+#datasource.username=
+#datasource.password=
+
+
+datasource.driverClassName=com.mysql.cj.jdbc.Driver
+datasource.url=jdbc:mysql://localhost:3306/newdg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+datasource.username=root
+datasource.password=123456
+
+#datasource.driverClassName=com.mysql.cj.jdbc.Driver
+#datasource.url=jdbc:mysql://localhost:3306/newdg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+#datasource.username=root
+#datasource.password=123456
+
+#数据库方言,可选项:derby、mysql、oracle、postgresql、default,留空则表示自动判断
+datasourceDialect=mysql
+
+#服务层缓存配置:
+#是否禁用缓存:true 禁用;false 启用
+service.cache.disabled=false
+#缓存配置项
+#maximumSize 缓存容量,默认1000
+#expireAfterAccess 过期时间,默认3天(跨周末)
+service.cache.spec=maximumSize=1000,expireAfterAccess=3d
+
+#看板分享密码加密配置:
+#注意:修改这两项配置会导致系统内所有设置分享密码的看板在访问时校验密码失败,需登录系统重新设置所有看板分享密码!!!
+#密钥,默认为"DataGear"的base64编码值
+dashboardSharePassword.crypto.secretKey=RGF0YUdlYXI=
+#盐值,应仅包含0-9、a-f、A-F字符、且长度为偶数的字符串,默认为"DataGear"的hex编码值
+dashboardSharePassword.crypto.salt=4461746147656172
+#看板访问密码允许填错次数,-1表示不限制
+dashboardSharePassword.authFailThreshold=5
+#看板访问密码允许填错次数的限定分钟数
+dashboardSharePassword.authFailPastMinutes=60
+
+#Spring Boot配置
+#-----------------------------------------
+
+#内嵌服务端口号
+server.port=50402
+server.servlet.session.cookie.name=qinnamin
+
+lab2=https://blog.csdn.net/weixin_41129148/article/details/121208602
+#-----------------------------------------

+ 88 - 0
datagear-analysis/pom.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.datagear</groupId>
+		<artifactId>datagear</artifactId>
+		<version>3.0.0</version>
+	</parent>
+	
+	<artifactId>datagear-analysis</artifactId>
+	<name>datagear-analysis</name>
+	
+	<dependencies>
+		<dependency>
+			<groupId>org.datagear</groupId>
+			<artifactId>datagear-util</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.github.ben-manes.caffeine</groupId>
+			<artifactId>caffeine</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.freemarker</groupId>
+			<artifactId>freemarker</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.jsoup</groupId>
+			<artifactId>jsoup</artifactId>
+			<version>1.10.2</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-csv</artifactId>
+			<version>${commons-csv.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi</artifactId>
+			<version>${poi.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.poi</groupId>
+			<artifactId>poi-ooxml</artifactId>
+			<version>${poi-ooxml.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents.client5</groupId>
+			<artifactId>httpclient5</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.jayway.jsonpath</groupId>
+			<artifactId>json-path</artifactId>
+		</dependency>
+	</dependencies>
+	
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+			    <artifactId>maven-antrun-plugin</artifactId>
+			    <version>${maven-antrun-plugin.version}</version>
+			    <executions>
+			    	<!-- 拷贝LICENSE文件 -->
+			    	<execution>
+			    		<id>copyLICENSE</id>
+			    		<phase>prepare-package</phase>
+			    		<goals>
+			    			<goal>run</goal>
+			    		</goals>
+			    		<configuration>
+			    			<tasks>
+			    				<copy file="../LICENSE" todir="${project.build.outputDirectory}" />
+			    			</tasks>
+			    		</configuration>
+			    	</execution>
+			    </executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>

+ 61 - 0
datagear-analysis/src/main/java/org/datagear/analysis/AbstractDataNameType.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+
+/**
+ * 抽象{@linkplain DataNameType}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractDataNameType implements DataNameType, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 名称 */
+	private String name;
+
+	/** 类型 */
+	private String type;
+
+	public AbstractDataNameType()
+	{
+		super();
+	}
+
+	public AbstractDataNameType(String name, String type)
+	{
+		super();
+		this.name = name;
+		this.type = type;
+	}
+
+	@Override
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	@Override
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setType(String type)
+	{
+		this.type = type;
+	}
+}

+ 76 - 0
datagear-analysis/src/main/java/org/datagear/analysis/AbstractIdentifiable.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 抽象{@linkplain Identifiable}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class AbstractIdentifiable implements Identifiable
+{
+	private String id;
+
+	public AbstractIdentifiable()
+	{
+		super();
+	}
+
+	public AbstractIdentifiable(String id)
+	{
+		super();
+		this.id = id;
+	}
+
+	@Override
+	public String getId()
+	{
+		return id;
+	}
+
+	public void setId(String id)
+	{
+		this.id = id;
+	}
+
+	@Override
+	public int hashCode()
+	{
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		AbstractIdentifiable other = (AbstractIdentifiable) obj;
+		if (id == null)
+		{
+			if (other.id != null)
+				return false;
+		}
+		else if (!id.equals(other.id))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [id=" + id + "]";
+	}
+}

+ 100 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Category.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+
+import org.datagear.util.i18n.AbstractLabeled;
+import org.datagear.util.i18n.Labeled;
+
+/**
+ * 图表插件类别。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Category extends AbstractLabeled implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_NAME = "name";
+	public static final String PROPERTY_NAME_LABEL = Labeled.PROPERTY_NAME_LABEL;
+	public static final String PROPERTY_DESC_LABEL = Labeled.PROPERTY_DESC_LABEL;
+	public static final String PROPERTY_ORDER = "order";
+
+	private String name;
+
+	private int order = 0;
+
+	public Category()
+	{
+		super();
+	}
+
+	public Category(String name)
+	{
+		super();
+		this.name = name;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public int getOrder()
+	{
+		return order;
+	}
+
+	public void setOrder(int order)
+	{
+		this.order = order;
+	}
+
+	@Override
+	public int hashCode()
+	{
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((name == null) ? 0 : name.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Category other = (Category) obj;
+		if (name == null)
+		{
+			if (other.name != null)
+				return false;
+		}
+		else if (!name.equals(other.name))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + name + ", nameLabel=" + getNameLabel() + ", descLabel="
+				+ getDescLabel() + ", order=" + order + "]";
+	}
+}

+ 67 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Chart.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 图表。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Chart extends ChartDefinition
+{
+	private static final long serialVersionUID = 1L;
+
+	private ChartPlugin plugin;
+
+	private transient RenderContext renderContext;
+
+	public Chart()
+	{
+		super();
+	}
+
+	public Chart(String id, String name, ChartDataSet[] chartDataSets, ChartPlugin plugin, RenderContext renderContext)
+	{
+		super(id, name, chartDataSets);
+		this.plugin = plugin;
+		this.renderContext = renderContext;
+	}
+
+	public Chart(ChartDefinition chartDefinition, ChartPlugin plugin, RenderContext renderContext)
+	{
+		super(chartDefinition);
+		this.plugin = plugin;
+		this.renderContext = renderContext;
+	}
+
+	public Chart(Chart chart)
+	{
+		this(chart, chart.plugin, chart.renderContext);
+	}
+
+	public RenderContext getRenderContext()
+	{
+		return renderContext;
+	}
+
+	public void setRenderContext(RenderContext renderContext)
+	{
+		this.renderContext = renderContext;
+	}
+
+	public ChartPlugin getPlugin()
+	{
+		return plugin;
+	}
+
+	public void setPlugin(ChartPlugin plugin)
+	{
+		this.plugin = plugin;
+	}
+}

+ 174 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartDataSet.java

@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 图表数据集。
+ * <p>
+ * 此类描述图表关联的某个{@linkplain DataSet}、及相关设置信息。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartDataSet implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 数据集 */
+	private DataSet dataSet;
+
+	/** 数据集属性标记映射表 */
+	private Map<String, Set<String>> propertySigns = Collections.emptyMap();
+
+	/** 数据集别名 */
+	private String alias = "";
+
+	/** 是否附件数据集,不用作渲染图表 */
+	private boolean attachment = false;
+
+	/** 数据集查询 */
+	private DataSetQuery query = null;
+
+	/** 数据集属性别名映射表 */
+	private Map<String, String> propertyAliases = Collections.emptyMap();
+
+	/** 数据集属性排序 */
+	private Map<String, ? extends Number> propertyOrders = Collections.emptyMap();
+
+	public ChartDataSet()
+	{
+		super();
+	}
+
+	public ChartDataSet(DataSet dataSet)
+	{
+		super();
+		this.dataSet = dataSet;
+	}
+
+	public DataSet getDataSet()
+	{
+		return dataSet;
+	}
+
+	public void setDataSet(DataSet dataSet)
+	{
+		this.dataSet = dataSet;
+	}
+
+	/**
+	 * 获取数据集属性标记映射表,其关键字是{@linkplain #getDataSet()}的{@linkplain DataSetProperty#getName()}、
+	 * 值则{@linkplain Chart#getPlugin()}的{@linkplain ChartPlugin#getDataSigns()}的{@linkplain DataSign#getName()}集合。
+	 * 
+	 * @return
+	 */
+	public Map<String, Set<String>> getPropertySigns()
+	{
+		return propertySigns;
+	}
+
+	public void setPropertySigns(Map<String, Set<String>> propertySigns)
+	{
+		this.propertySigns = propertySigns;
+	}
+
+	/**
+	 * 获取数据集别名。
+	 * <p>
+	 * 一个图表可能多次包含同一个数据集,此别名可在图表展示时用于区分显示。
+	 * </p>
+	 * 
+	 * @return 返回{@code null}或空表示无别名
+	 */
+	public String getAlias()
+	{
+		return alias;
+	}
+
+	public void setAlias(String alias)
+	{
+		this.alias = alias;
+	}
+
+	/**
+	 * 是否作为附件。
+	 * <p>
+	 * 附件数据集不用作渲染图表,不需设置数据标记,仅作为图表的附件,通常用于扩展图表功能。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	public boolean isAttachment()
+	{
+		return attachment;
+	}
+
+	public void setAttachment(boolean attachment)
+	{
+		this.attachment = attachment;
+	}
+
+	public DataSetQuery getQuery()
+	{
+		return query;
+	}
+
+	public void setQuery(DataSetQuery query)
+	{
+		this.query = query;
+	}
+
+	public Map<String, String> getPropertyAliases()
+	{
+		return propertyAliases;
+	}
+
+	public void setPropertyAliases(Map<String, String> propertyAliases)
+	{
+		this.propertyAliases = propertyAliases;
+	}
+
+	public Map<String, ? extends Number> getPropertyOrders()
+	{
+		return propertyOrders;
+	}
+
+	public void setPropertyOrders(Map<String, ? extends Number> propertyOrders)
+	{
+		this.propertyOrders = propertyOrders;
+	}
+
+	/**
+	 * 使用{@linkplain #getQuery()}获取{@linkplain #getDataSet()}的{@linkplain DataSetResult}。
+	 * 
+	 * @return
+	 * @throws DataSetException
+	 */
+	public DataSetResult getResult() throws DataSetException
+	{
+		return this.dataSet.getResult(this.query);
+	}
+
+	/**
+	 * 获取{@linkplain #getDataSet()}的{@linkplain DataSetResult}。
+	 * 
+	 * @param query
+	 * @return
+	 * @throws DataSetException
+	 */
+	public DataSetResult getResult(DataSetQuery query) throws DataSetException
+	{
+		return this.dataSet.getResult(query);
+	}
+}

+ 250 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartDefinition.java

@@ -0,0 +1,250 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 图表定义。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartDefinition extends AbstractIdentifiable implements ResultDataFormatAware, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_ID = "id";
+	public static final String PROPERTY_NAME = "name";
+	public static final String PROPERTY_CHART_DATASETS = "chartDataSets";
+	public static final String PROPERTY_CHART_ATTRIBUTES = "attributes";
+	public static final String PROPERTY_UPDATE_INTERVAL = "updateInterval";
+
+	public static final ChartDataSet[] EMPTY_CHART_DATA_SET = new ChartDataSet[0];
+
+	/** 图表名称 */
+	private String name;
+
+	/** 图表数据集 */
+	private ChartDataSet[] chartDataSets = EMPTY_CHART_DATA_SET;
+
+	/** 图表属性映射表 */
+	@SuppressWarnings("unchecked")
+	private Map<String, Object> attributes = Collections.EMPTY_MAP;
+
+	/** 图表更新间隔毫秒数 */
+	private int updateInterval = -1;
+
+	/**结果数据格式*/
+	private ResultDataFormat resultDataFormat = null;
+	
+	public ChartDefinition()
+	{
+		super();
+	}
+
+	public ChartDefinition(String id, String name, ChartDataSet[] chartDataSets)
+	{
+		super(id);
+		this.name = name;
+		this.chartDataSets = chartDataSets;
+	}
+
+	public ChartDefinition(ChartDefinition chartDefinition)
+	{
+		this(chartDefinition.getId(), chartDefinition.name, chartDefinition.chartDataSets);
+		this.attributes = chartDefinition.attributes;
+		this.updateInterval = chartDefinition.updateInterval;
+		this.resultDataFormat = chartDefinition.resultDataFormat;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public ChartDataSet[] getChartDataSets()
+	{
+		return chartDataSets;
+	}
+
+	public void setChartDataSets(ChartDataSet[] chartDataSets)
+	{
+		this.chartDataSets = chartDataSets;
+	}
+
+	/**
+	 * 获取图表属性映射表。
+	 * 
+	 * @return
+	 */
+	public Map<String, Object> getAttributes()
+	{
+		return attributes;
+	}
+
+	/**
+	 * 设置图表属性映射表。
+	 * 
+	 * @param attributes
+	 */
+	public void setAttributes(Map<String, Object> attributes)
+	{
+		this.attributes = attributes;
+	}
+
+	/**
+	 * 设置属性。
+	 * 
+	 * @param name
+	 * @param value
+	 */
+	public void setAttribute(String name, Object value)
+	{
+		if (this.attributes == null || this.attributes == Collections.EMPTY_MAP)
+			this.attributes = new HashMap<>();
+
+		this.attributes.put(name, value);
+	}
+
+	/**
+	 * 获取属性。
+	 * 
+	 * @param name
+	 * @return 返回{@code null}表示没有。
+	 */
+	public Object getAttribute(String name)
+	{
+		if (this.attributes == null)
+			return null;
+
+		return this.attributes.get(name);
+	}
+
+	/**
+	 * 获取图表更新间隔毫秒数。
+	 * 
+	 * @return {@code <0}:不更新;0 :实时更新;{@code >0}:间隔更新毫秒数
+	 */
+	public int getUpdateInterval()
+	{
+		return updateInterval;
+	}
+
+	public void setUpdateInterval(int updateInterval)
+	{
+		this.updateInterval = updateInterval;
+	}
+
+	/**
+	 * 获取图表数据集结果数据格式。
+	 * 
+	 * @return
+	 */
+	@Override
+	public ResultDataFormat getResultDataFormat()
+	{
+		return resultDataFormat;
+	}
+
+	/**
+	 * 设置图表数据集结果数据格式。
+	 * 
+	 * @param dataFormat
+	 */
+	@Override
+	public void setResultDataFormat(ResultDataFormat resultDataFormat)
+	{
+		this.resultDataFormat = resultDataFormat;
+	}
+	
+	/**
+	 * 获取{@linkplain ChartResult}。
+	 * <p>
+	 * 如果{@linkplain ChartQuery#getDataSetQuery(int)}为{@code null},此方法将使用{@linkplain ChartDataSet#getQuery()}。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws DataSetException
+	 */
+	public ChartResult getResult(ChartQuery query) throws DataSetException
+	{
+		if (this.chartDataSets == null || this.chartDataSets.length == 0)
+			return new ChartResult(Collections.emptyList());
+
+		ChartResult chartResult = new ChartResult();
+
+		List<DataSetResult> dataSetResults = new ArrayList<DataSetResult>(this.chartDataSets.length);
+
+		for (int i = 0; i < this.chartDataSets.length; i++)
+		{
+			ChartDataSet chartDataSet = this.chartDataSets[i];
+			DataSetQuery dataSetQuery = getDataSetQuery(query, chartDataSet, i);
+			DataSetResult dataSetResult = chartDataSet.getResult(dataSetQuery);
+
+			dataSetResults.add(dataSetResult);
+		}
+
+		chartResult.setDataSetResults(dataSetResults);
+
+		return chartResult;
+	}
+
+	/**
+	 * 获取指定{@linkplain DataSetQuery}。
+	 * 
+	 * @param chartQuery
+	 * @param chartDataSet
+	 * @param chartDataSetIdx
+	 * @return
+	 */
+	protected DataSetQuery getDataSetQuery(ChartQuery chartQuery, ChartDataSet chartDataSet, int chartDataSetIdx)
+	{
+		DataSetQuery dataSetQuery = chartQuery.getDataSetQuery(chartDataSetIdx);
+
+		if (dataSetQuery == null)
+			dataSetQuery = chartDataSet.getQuery();
+
+		if (dataSetQuery == null)
+			dataSetQuery = DataSetQuery.valueOf();
+
+		if (dataSetQuery.getResultDataFormat() == null)
+		{
+			ResultDataFormat cqrds = chartQuery.getResultDataFormat();
+			ResultDataFormat crds = getResultDataFormat();
+
+			if (cqrds != null || crds != null)
+			{
+				dataSetQuery = dataSetQuery.copy();
+				dataSetQuery.setResultDataFormat(cqrds);
+
+				if (dataSetQuery.getResultDataFormat() == null)
+					dataSetQuery.setResultDataFormat(crds);
+			}
+		}
+
+		return dataSetQuery;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [id=" + getId() + ", name=" + name + "]";
+	}
+}

+ 109 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartParam.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+
+import org.datagear.util.i18n.AbstractLabeled;
+import org.datagear.util.i18n.Labeled;
+
+/**
+ * 图表参数。
+ * <p>
+ * 此类描述{@linkplain ChartPlugin}创建{@linkplain Chart}所需要的输入参数信息。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartParam extends AbstractLabeled implements DataNameType, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_NAME = "name";
+	public static final String PROPERTY_TYPE = "type";
+	public static final String PROPERTY_NAME_LABEL = Labeled.PROPERTY_NAME_LABEL;
+	public static final String PROPERTY_DESC_LABEL = Labeled.PROPERTY_DESC_LABEL;
+
+	/** 名称 */
+	private String name;
+
+	/** 类型 */
+	private String type;
+
+	/** 是否必须 */
+	private boolean required;
+
+	public ChartParam()
+	{
+	}
+
+	public ChartParam(String name, String type, boolean required)
+	{
+		super();
+		this.name = name;
+		this.type = type;
+		this.required = required;
+	}
+
+	@Override
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	@Override
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setType(String type)
+	{
+		this.type = type;
+	}
+
+	public boolean isRequired()
+	{
+		return required;
+	}
+
+	public void setRequired(boolean required)
+	{
+		this.required = required;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + name + ", type=" + type + "]";
+	}
+
+	/**
+	 * {@linkplain ChartParam#getType()}枚举。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class DataType
+	{
+		/** 字符串 */
+		public static final String STRING = "STRING";
+
+		/** 布尔值 */
+		public static final String BOOLEAN = "BOOLEAN";
+
+		/** 整数 */
+		public static final String NUMBER = "NUMBER";
+	}
+}

+ 153 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartPlugin.java

@@ -0,0 +1,153 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.util.i18n.Label;
+import org.datagear.util.i18n.Labeled;
+
+/**
+ * 图表插件。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface ChartPlugin extends Identifiable, Labeled, Serializable
+{
+	String PROPERTY_ID = "id";
+	String PROPERTY_NAME_LABEL = Labeled.PROPERTY_NAME_LABEL;
+	String PROPERTY_DESC_LABEL = Labeled.PROPERTY_DESC_LABEL;
+	String PROPERTY_MANUAL_LABEL = "manualLabel";
+	String PROPERTY_ICONS = "icons";
+	String PROPERTY_CHART_PARAMS = "chartParams";
+	String PROPERTY_DATA_SIGNS = "dataSigns";
+	String PROPERTY_VERSION = "version";
+	String PROPERTY_ORDER = "order";
+	String PROPERTY_CATEGORY = "category";
+
+	/** 默认图标主题名 */
+	String DEFAULT_ICON_THEME_NAME = "default";
+
+	/**
+	 * 获取名称标签。
+	 * <p>
+	 * 此方法不应返回{@code null}。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	@Override
+	Label getNameLabel();
+
+	/**
+	 * 获取使用指南标签。
+	 * <p>
+	 * 返回{@code null}表示无使用标签。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	Label getManualLabel();
+
+	/**
+	 * 获取所有风格图标。
+	 * <p>
+	 * 返回{@code null}或空表示无图标。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	Map<String, Icon> getIcons();
+
+	/**
+	 * 获取指定主题名称的图标,没有找到则获取{@linkplain #DEFAULT_ICON_THEME_NAME}对应的图标,没有找到则返回{@code null}。
+	 * 
+	 * @param themeName
+	 * @return
+	 */
+	Icon getIcon(String themeName);
+
+	/**
+	 * 获取{@linkplain ChartParam}列表。
+	 * <p>
+	 * 返回{@code null}表示没有。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	List<ChartParam> getChartParams();
+
+	/**
+	 * 获取指定名称的{@linkplain ChartParam},没有找到则返回{@code null}。
+	 * 
+	 * @param name
+	 * @return
+	 */
+	ChartParam getChartParam(String name);
+
+	/**
+	 * 获取{@linkplain DataSign}列表。
+	 * <p>
+	 * 返回{@code null}表示没有。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	List<DataSign> getDataSigns();
+
+	/**
+	 * 获取指定名称的{@linkplain DataSign},没有找到则返回{@code null}。
+	 * 
+	 * @param name
+	 * @return
+	 */
+	DataSign getDataSign(String name);
+
+	/**
+	 * 渲染{@linkplain Chart}。
+	 * 
+	 * @param renderContext
+	 * @param chartDefinition
+	 * @return
+	 * @throws RenderException
+	 */
+	Chart renderChart(RenderContext renderContext, ChartDefinition chartDefinition) throws RenderException;
+
+	/**
+	 * 获取版本号。
+	 * <p>
+	 * 版本号格式应为:<code>[主版本号].[次版本号].[修订版本号]</code>。
+	 * </p>
+	 * <p>
+	 * 返回{@code null}或空字符串表示无版本号标识。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	String getVersion();
+
+	/**
+	 * 获取排序值。
+	 * <p>
+	 * {@linkplain ChartPluginManager#getAll()}、和{@linkplain ChartPluginManager#getAll(Class)}使用此值进行排序,越小越靠前。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	int getOrder();
+
+	/**
+	 * 获取所属类别。
+	 * 
+	 * @return 返回{@code null}表示无类别
+	 */
+	Category getCategory();
+}

+ 63 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartPluginManager.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.List;
+
+/**
+ * {@linkplain ChartPlugin}管理器。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface ChartPluginManager
+{
+	/**
+	 * 注册一个{@linkplain ChartPlugin}。
+	 * 
+	 * @param chartPlugin
+	 */
+	void register(ChartPlugin chartPlugin);
+
+	/**
+	 * 移除指定ID的{@linkplain ChartPlugin}。
+	 * 
+	 * @param ids
+	 * @return 被移除的{@linkplain ChartPlugin}或者{@code null}。
+	 */
+	ChartPlugin[] remove(String... ids);
+
+	/**
+	 * 获取指定ID的{@linkplain ChartPlugin}。
+	 * 
+	 * @param id
+	 * @return
+	 */
+	ChartPlugin get(String id);
+
+	/**
+	 * 获取指定类型的所有{@linkplain ChartPlugin}。
+	 * <p>
+	 * 返回结果将根据{@linkplain ChartPlugin#getOrder()}进行排序,越小越靠前。
+	 * </p>
+	 * 
+	 * @param chartPluginType
+	 * @return
+	 */
+	<T extends ChartPlugin> List<T> getAll(Class<? super T> chartPluginType);
+
+	/**
+	 * 获取所有{@linkplain ChartPlugin}。
+	 * <p>
+	 * 返回结果将根据{@linkplain ChartPlugin#getOrder()}进行排序,越小越靠前。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	List<ChartPlugin> getAll();
+}

+ 92 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartQuery.java

@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 图表查询。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartQuery implements ResultDataFormatAware, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	private List<DataSetQuery> dataSetQueries = Collections.emptyList();
+
+	/** 图表结果数格式 */
+	private ResultDataFormat resultDataFormat = null;
+
+	public ChartQuery()
+	{
+		super();
+	}
+
+	public ChartQuery(List<DataSetQuery> dataSetQueries)
+	{
+		super();
+		this.dataSetQueries = dataSetQueries;
+	}
+
+	public ChartQuery(ChartQuery chartQuery)
+	{
+		super();
+		this.dataSetQueries = chartQuery.dataSetQueries;
+		this.resultDataFormat = chartQuery.resultDataFormat;
+	}
+
+	public List<DataSetQuery> getDataSetQueries()
+	{
+		return dataSetQueries;
+	}
+
+	public void setDataSetQueries(List<DataSetQuery> dataSetQueries)
+	{
+		this.dataSetQueries = dataSetQueries;
+	}
+
+	@Override
+	public ResultDataFormat getResultDataFormat()
+	{
+		return resultDataFormat;
+	}
+
+	@Override
+	public void setResultDataFormat(ResultDataFormat resultDataFormat)
+	{
+		this.resultDataFormat = resultDataFormat;
+	}
+
+	/**
+	 * 获取指定索引的{@linkplain DataSetQuery}。
+	 * <p>
+	 * 此方法不会抛出索引越界异常,如果索引越界,将直接返回{@code null}。
+	 * </p>
+	 * 
+	 * @param index
+	 * @return 返回{@code null}表示无对应的{@linkplain DataSetQuery}
+	 */
+	public DataSetQuery getDataSetQuery(int index)
+	{
+		int size = (this.dataSetQueries == null ? 0 : this.dataSetQueries.size());
+
+		if (index >= size)
+			return null;
+
+		return this.dataSetQueries.get(index);
+	}
+
+	public ChartQuery copy()
+	{
+		return new ChartQuery(this);
+	}
+}

+ 51 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartResult.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 图表结果。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartResult
+{
+	private List<DataSetResult> dataSetResults = Collections.emptyList();
+
+	public ChartResult()
+	{
+		super();
+	}
+
+	public ChartResult(List<DataSetResult> dataSetResults)
+	{
+		super();
+		this.dataSetResults = dataSetResults;
+	}
+
+	/**
+	 * 获取{@linkplain DataSetResult}列表。
+	 * <p>
+	 * 返回列表的元素与{@linkplain ChartDefinition#getChartDataSets()}元素一一对应。
+	 * </p>
+	 * 
+	 * @return 返回{@code null}或空列表表示无结果
+	 */
+	public List<DataSetResult> getDataSetResults()
+	{
+		return dataSetResults;
+	}
+
+	public void setDataSetResults(List<DataSetResult> dataSetResults)
+	{
+		this.dataSetResults = dataSetResults;
+	}
+}

+ 40 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartResultError.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 获取图表结果错误信息。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartResultError
+{
+	private Throwable throwable;
+
+	public ChartResultError()
+	{
+		super();
+	}
+
+	public ChartResultError(Throwable throwable)
+	{
+		super();
+		this.throwable = throwable;
+	}
+
+	public Throwable getThrowable()
+	{
+		return throwable;
+	}
+
+	public void setThrowable(Throwable throwable)
+	{
+		this.throwable = throwable;
+	}
+}

+ 207 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ChartTheme.java

@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+/**
+ * 图表主题。
+ * <p>
+ * {@linkplain #getTitleTheme()}、{@linkplain #getLegendTheme()}、{@linkplain #getTooltipTheme()}、{@linkplain #getHighlightTheme()}不是必填的,
+ * 它们可以由展现界面根据{@linkplain #getColor()}、{@linkplain #getActualBackgroundColor()}配合{@linkplain #getGradient()}自动生成。
+ * </p>
+ * <p>
+ * 此类可为在看板内绘制统一主题的多个图表提供支持。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartTheme extends Theme implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 透明颜色值常量 */
+	public static final String COLOR_TRANSPARENT = "transparent";
+
+	/** 实际背景色 */
+	private String actualBackgroundColor;
+
+	/** 图形条目颜色 */
+	private String[] graphColors;
+
+	/** 值域映射范围图形条目颜色 */
+	private String[] graphRangeColors;
+
+	/** 背景色至前景色的渐变跨度 */
+	private int gradient = 10;
+
+	/** 标题主题 */
+	private Theme titleTheme = null;
+
+	/** 图例主题 */
+	private Theme legendTheme = null;
+
+	/** 提示框主题 */
+	private Theme tooltipTheme = null;
+
+	/** 高亮区主题 */
+	private Theme highlightTheme = null;
+
+	public ChartTheme()
+	{
+	}
+
+	public ChartTheme(String name, String color, String backgroundColor, String actualBackgroundColor,
+			String[] graphColors, String[] graphRangeColors)
+	{
+		super(name, color, backgroundColor);
+		this.setActualBackgroundColor(actualBackgroundColor);
+		this.graphColors = graphColors;
+		this.graphRangeColors = graphRangeColors;
+	}
+
+	@Override
+	public void setBackgroundColor(String backgroundColor)
+	{
+		super.setBackgroundColor(backgroundColor);
+
+		if (!COLOR_TRANSPARENT.equalsIgnoreCase(actualBackgroundColor))
+			this.actualBackgroundColor = backgroundColor;
+	}
+
+	/**
+	 * 获取实际背景色。
+	 * <p>
+	 * 实际背景色不会是透明色{@linkplain #COLOR_TRANSPARENT}。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	public String getActualBackgroundColor()
+	{
+		return actualBackgroundColor;
+	}
+
+	/**
+	 * 设置实际背景色。
+	 * 
+	 * @param actualBackgroundColor
+	 * @throws IllegalArgumentException 当参数为{@linkplain #COLOR_TRANSPARENT}时
+	 */
+	public void setActualBackgroundColor(String actualBackgroundColor) throws IllegalArgumentException
+	{
+		if (COLOR_TRANSPARENT.equalsIgnoreCase(actualBackgroundColor))
+			throw new IllegalArgumentException("[actualBackgroundColor] must not be '" + COLOR_TRANSPARENT + "'");
+
+		this.actualBackgroundColor = actualBackgroundColor;
+	}
+
+	public String[] getGraphColors()
+	{
+		return graphColors;
+	}
+
+	public void setGraphColors(String[] graphColors)
+	{
+		this.graphColors = graphColors;
+	}
+
+	public String[] getGraphRangeColors()
+	{
+		return graphRangeColors;
+	}
+
+	public void setGraphRangeColors(String[] graphRangeColors)
+	{
+		this.graphRangeColors = graphRangeColors;
+	}
+
+	public int getGradient()
+	{
+		return gradient;
+	}
+
+	public void setGradient(int gradient)
+	{
+		this.gradient = gradient;
+	}
+
+	public boolean hasTitleTheme()
+	{
+		return (this.titleTheme != null);
+	}
+
+	public Theme getTitleTheme()
+	{
+		return titleTheme;
+	}
+
+	public void setTitleTheme(Theme titleTheme)
+	{
+		this.titleTheme = titleTheme;
+	}
+
+	public boolean hasLegendTheme()
+	{
+		return (this.legendTheme != null);
+	}
+
+	public Theme getLegendTheme()
+	{
+		return legendTheme;
+	}
+
+	public void setLegendTheme(Theme legendTheme)
+	{
+		this.legendTheme = legendTheme;
+	}
+
+	public boolean hasTooltipTheme()
+	{
+		return (this.tooltipTheme != null);
+	}
+
+	public Theme getTooltipTheme()
+	{
+		return tooltipTheme;
+	}
+
+	public void setTooltipTheme(Theme tooltipTheme)
+	{
+		this.tooltipTheme = tooltipTheme;
+	}
+
+	public boolean hasHighlightTheme()
+	{
+		return (this.highlightTheme != null);
+	}
+
+	public Theme getHighlightTheme()
+	{
+		return highlightTheme;
+	}
+
+	public void setHighlightTheme(Theme highlightTheme)
+	{
+		this.highlightTheme = highlightTheme;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + getName() + ", color=" + getColor() + ", backgroundColor="
+				+ getBackgroundColor() + ", borderColor=" + getBorderColor() + ", borderWidth="
+				+ getBorderWidth() + ", fontSize=" + getFontSize() + ", actualBackgroundColor="
+				+ actualBackgroundColor + ", graphColors=" + Arrays.toString(graphColors) + ", graphRangeColors="
+				+ Arrays.toString(graphRangeColors) + ", gradient=" + gradient + ", titleTheme=" + titleTheme
+				+ ", legendTheme=" + legendTheme + ", tooltipTheme=" + tooltipTheme + ", highlightTheme="
+				+ highlightTheme + "]";
+	}
+}

+ 160 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Dashboard.java

@@ -0,0 +1,160 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 看板。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Dashboard extends DashboardQueryHandler implements Identifiable, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_ID = "id";
+	public static final String PROPERTY_RENDER_CONTEXT = "renderContext";
+	public static final String PROPERTY_WIDGET = "widget";
+	public static final String PROPERTY_CHARTS = "charts";
+
+	private String id;
+
+	private transient RenderContext renderContext;
+
+	private DashboardWidget widget;
+
+	private List<Chart> charts = null;
+
+	public Dashboard()
+	{
+		super();
+	}
+
+	public Dashboard(String id, RenderContext renderContext, DashboardWidget widget)
+	{
+		super();
+		this.id = id;
+		this.renderContext = renderContext;
+		this.widget = widget;
+	}
+
+	@Override
+	public String getId()
+	{
+		return id;
+	}
+
+	public void setId(String id)
+	{
+		this.id = id;
+	}
+
+	public RenderContext getRenderContext()
+	{
+		return renderContext;
+	}
+
+	public void setRenderContext(RenderContext renderContext)
+	{
+		this.renderContext = renderContext;
+	}
+
+	public DashboardWidget getWidget()
+	{
+		return widget;
+	}
+
+	public void setWidget(DashboardWidget widget)
+	{
+		this.widget = widget;
+	}
+
+	/**
+	 * 是否包含图表。
+	 * 
+	 * @return
+	 */
+	public boolean hasChart()
+	{
+		return (this.charts != null && !this.charts.isEmpty());
+	}
+
+	public List<Chart> getCharts()
+	{
+		return charts;
+	}
+
+	public void setCharts(List<Chart> charts)
+	{
+		this.charts = charts;
+	}
+
+	/**
+	 * 获取指定ID的{@linkplain Chart}。
+	 * 
+	 * @param chartId
+	 * @return 返回{@code null}表示没有找到
+	 */
+	public Chart getChart(String chartId)
+	{
+		if (this.charts == null)
+			return null;
+
+		for (Chart chart : this.charts)
+		{
+			if (chart.getId().equals(chartId))
+				return chart;
+		}
+
+		return null;
+	}
+
+	@Override
+	protected ChartDefinition getChartDefinition(String chartId)
+	{
+		return getChart(chartId);
+	}
+
+	@Override
+	public int hashCode()
+	{
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Dashboard other = (Dashboard) obj;
+		if (id == null)
+		{
+			if (other.id != null)
+				return false;
+		}
+		else if (!id.equals(other.id))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [id=" + id + "]";
+	}
+}

+ 87 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardQuery.java

@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * 看板查询。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DashboardQuery implements ResultDataFormatAware, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 图表ID-查询映射表 */
+	private Map<String, ChartQuery> chartQueries = Collections.emptyMap();
+
+	/** 图表结果数格式 */
+	private ResultDataFormat resultDataFormat = null;
+
+	/** 单个图表查询出错时是否不抛出异常,而仅记录错误信息 */
+	private boolean suppressChartError = false;
+
+	public DashboardQuery()
+	{
+		super();
+	}
+
+	public DashboardQuery(Map<String, ChartQuery> chartQueries)
+	{
+		super();
+		this.chartQueries = chartQueries;
+	}
+
+	public DashboardQuery(DashboardQuery query)
+	{
+		super();
+		this.chartQueries = query.chartQueries;
+		this.suppressChartError = query.suppressChartError;
+	}
+
+	public Map<String, ChartQuery> getChartQueries()
+	{
+		return chartQueries;
+	}
+
+	public void setChartQueries(Map<String, ChartQuery> chartQueries)
+	{
+		this.chartQueries = chartQueries;
+	}
+
+	@Override
+	public ResultDataFormat getResultDataFormat()
+	{
+		return resultDataFormat;
+	}
+
+	@Override
+	public void setResultDataFormat(ResultDataFormat resultDataFormat)
+	{
+		this.resultDataFormat = resultDataFormat;
+	}
+
+	public boolean isSuppressChartError()
+	{
+		return suppressChartError;
+	}
+
+	public void setSuppressChartError(boolean suppressChartError)
+	{
+		this.suppressChartError = suppressChartError;
+	}
+
+	public DashboardQuery copy()
+	{
+		return new DashboardQuery(this);
+	}
+}

+ 91 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardQueryHandler.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@linkplain DashboardQuery}处理器。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class DashboardQueryHandler
+{
+	public DashboardQueryHandler()
+	{
+		super();
+	}
+
+	/**
+	 * 获取{@linkplain DashboardResult}。
+	 * 
+	 * @param query
+	 * @return
+	 * @throws DataSetException
+	 */
+	public DashboardResult getResult(DashboardQuery query) throws DataSetException
+	{
+		Map<String, ChartQuery> chartQueries = query.getChartQueries();
+		boolean suppressChartError = query.isSuppressChartError();
+
+		Map<String, ChartResult> chartResults = new HashMap<String, ChartResult>(chartQueries.size());
+		Map<String, ChartResultError> chartResultErrors = new HashMap<String, ChartResultError>();
+
+		for (Map.Entry<String, ChartQuery> entry : chartQueries.entrySet())
+		{
+			String chartId = entry.getKey();
+			ChartQuery chartQuery = entry.getValue();
+			ChartDefinition chart = getChartDefinition(chartId);
+
+			if (chart == null)
+				throw new IllegalArgumentException("Chart '" + chartId + "' not found");
+
+			if (chartQuery.getResultDataFormat() == null && query.getResultDataFormat() != null)
+			{
+				chartQuery = chartQuery.copy();
+				chartQuery.setResultDataFormat(query.getResultDataFormat());
+			}
+
+			ChartResult chartResult = null;
+
+			if (suppressChartError)
+			{
+				try
+				{
+					chartResult = chart.getResult(chartQuery);
+					chartResults.put(chartId, chartResult);
+				}
+				catch (Throwable t)
+				{
+					chartResultErrors.put(chartId, new ChartResultError(t));
+				}
+			}
+			else
+			{
+				chartResult = chart.getResult(chartQuery);
+				chartResults.put(chartId, chartResult);
+			}
+		}
+
+		DashboardResult dashboardResult = new DashboardResult(chartResults);
+		dashboardResult.setChartResultErrors(chartResultErrors);
+
+		return dashboardResult;
+	}
+	
+	/**
+	 * 获取指定图表ID对应的{@linkplain ChartDefinition}。
+	 * 
+	 * @param chartId
+	 *            {@linkplain Chart#getId()}
+	 * @return 允许返回{@code null}
+	 */
+	protected abstract ChartDefinition getChartDefinition(String chartId);
+}

+ 67 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardResult.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * 看板结果。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DashboardResult
+{
+	/** 图表ID-图表结果映射表 */
+	private Map<String, ChartResult> chartResults = Collections.emptyMap();
+
+	/** 图表ID-图表结果异常映射表 */
+	private Map<String, ChartResultError> chartResultErrors = Collections.emptyMap();
+
+	public DashboardResult()
+	{
+		super();
+	}
+
+	public DashboardResult(Map<String, ChartResult> chartResults)
+	{
+		super();
+		this.chartResults = chartResults;
+	}
+
+	/**
+	 * 获取[图表ID-图表结果]映射表。
+	 * 
+	 * @return
+	 */
+	public Map<String, ChartResult> getChartResults()
+	{
+		return chartResults;
+	}
+
+	public void setChartResults(Map<String, ChartResult> chartResults)
+	{
+		this.chartResults = chartResults;
+	}
+
+	/**
+	 * 获取[图表ID-{@linkplain ChartResultError}]映射表。
+	 * 
+	 * @return
+	 */
+	public Map<String, ChartResultError> getChartResultErrors()
+	{
+		return chartResultErrors;
+	}
+
+	public void setChartResultErrors(Map<String, ChartResultError> chartResultErrors)
+	{
+		this.chartResultErrors = chartResultErrors;
+	}
+}

+ 45 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardTheme.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 看板主题。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DashboardTheme extends Theme
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_CHART_THEME = "chartTheme";
+
+	private ChartTheme chartTheme;
+
+	public DashboardTheme()
+	{
+		super();
+	}
+
+	public DashboardTheme(String name, String color, String backgroundColor, ChartTheme chartTheme)
+	{
+		super(name, color, backgroundColor);
+		this.chartTheme = chartTheme;
+	}
+
+	public ChartTheme getChartTheme()
+	{
+		return chartTheme;
+	}
+
+	public void setChartTheme(ChartTheme chartTheme)
+	{
+		this.chartTheme = chartTheme;
+	}
+
+}

+ 32 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardThemeSource.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * {@linkplain DashboardTheme}源。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface DashboardThemeSource
+{
+	/**
+	 * 获取默认{@linkplain DashboardTheme}。
+	 * 
+	 * @return
+	 */
+	DashboardTheme getDashboardTheme();
+
+	/**
+	 * 获取指定名称的{@linkplain DashboardTheme},没有则返回{@code null}。
+	 * 
+	 * @param themeName
+	 * @return
+	 */
+	DashboardTheme getDashboardTheme(String themeName);
+}

+ 31 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DashboardWidget.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+
+/**
+ * 看板部件。
+ * <p>
+ * 它可在{@linkplain RenderContext}中渲染自己所描述的{@linkplain Dashboard}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface DashboardWidget extends Identifiable, Serializable
+{
+	/**
+	 * 渲染{@linkplain Dashboard}。
+	 * 
+	 * @param renderContext
+	 * @return
+	 * @throws RenderException
+	 */
+	Dashboard render(RenderContext renderContext) throws RenderException;
+}

+ 31 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataNameType.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 数据名、类型接口类。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface DataNameType
+{
+	/**
+	 * 获取名称。
+	 * 
+	 * @return
+	 */
+	String getName();
+
+	/**
+	 * 获取数据类型。
+	 * 
+	 * @return
+	 */
+	String getType();
+}

+ 90 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSet.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 数据集。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface DataSet extends Identifiable, Serializable
+{
+	/**
+	 * 获取名称。
+	 * 
+	 * @return
+	 */
+	String getName();
+
+	/**
+	 * 是否是易变模型。
+	 * <p>
+	 * 即{@linkplain #getResult(DataSetQuery)}返回数据的结构并不是固定不变、可由{@linkplain #getProperties()}描述的。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	boolean isMutableModel();
+
+	/**
+	 * 获取属性列表。
+	 * <p>
+	 * 属性列表描述{@linkplain #getResult(DataSetQuery)}返回的{@linkplain DataSetResult#getData()}的对象结构。
+	 * </p>
+	 * 
+	 * @return 属性列表,返回空列表则表示无属性
+	 */
+	List<DataSetProperty> getProperties();
+
+	/**
+	 * 获取指定名称的属性,没有则返回{@code null}。
+	 * 
+	 * @param name
+	 * @return
+	 */
+	DataSetProperty getProperty(String name);
+
+	/**
+	 * 获取参数列表。
+	 * 
+	 * @return 参数列表,返回空列表则表示无参数
+	 */
+	List<DataSetParam> getParams();
+
+	/**
+	 * 获取指定名称的参数,没有则返回{@code null}。
+	 * 
+	 * @param name
+	 * @return
+	 */
+	DataSetParam getParam(String name);
+
+	/**
+	 * 获取{@linkplain DataSetResult}。
+	 * <p>
+	 * 如果{@linkplain #isMutableModel()}为{@code false},那么返回结果中的数据项属性不应超出{@linkplain #getProperties()}的范围,
+	 * 避免暴露底层数据源不期望暴露的数据;
+	 * 如果{@linkplain #isMutableModel()}为{@code true},则返回结果中的数据项属性不受{@linkplain #getProperties()}范围限制。
+	 * </p>
+	 * <p>
+	 * 如果返回结果中的数据项属性在{@linkplain #getProperties()}中有对应,当数据项属性值为{@code null}时,应使用{@linkplain DataSetProperty#getDefaultValue()}的值。
+	 * </p>
+	 * <p>
+	 * 返回结果中的数据项属性值应已转换为与{@linkplain #getProperties()}的{@linkplain DataSetProperty#getType()}类型一致。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws DataSetException
+	 */
+	DataSetResult getResult(DataSetQuery query) throws DataSetException;
+}

+ 39 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSetException.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 数据集异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetException extends RuntimeException
+{
+	private static final long serialVersionUID = 1L;
+
+	public DataSetException()
+	{
+		super();
+	}
+
+	public DataSetException(String message)
+	{
+		super(message);
+	}
+
+	public DataSetException(Throwable cause)
+	{
+		super(cause);
+	}
+
+	public DataSetException(String message, Throwable cause)
+	{
+		super(message, cause);
+	}
+}

+ 158 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSetParam.java

@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 数据集参数。
+ * <p>
+ * 此类描述{@linkplain DataSet}获取{@linkplain DataSetResult}所需要的输入参数信息。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetParam extends AbstractDataNameType
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 是否必须 */
+	private boolean required;
+
+	/** 参数描述 */
+	private String desc;
+
+	/** 界面输入框类型 */
+	private String inputType;
+
+	/** 界面输入框载荷,比如:输入框为下拉选择时,定义选项内容JSON;输入概况为日期时,定义日期格式 */
+	private String inputPayload;
+
+	public DataSetParam()
+	{
+		super();
+	}
+
+	public DataSetParam(String name, String type, boolean required)
+	{
+		super(name, type);
+		this.required = required;
+	}
+
+	public boolean isRequired()
+	{
+		return required;
+	}
+
+	public void setRequired(boolean required)
+	{
+		this.required = required;
+	}
+
+	public boolean hasDesc()
+	{
+		return (this.desc != null && !this.desc.isEmpty());
+	}
+
+	public String getDesc()
+	{
+		return desc;
+	}
+
+	public void setDesc(String desc)
+	{
+		this.desc = desc;
+	}
+
+	public boolean hasInputType()
+	{
+		return (this.inputType != null && !this.inputType.isEmpty());
+	}
+
+	public String getInputType()
+	{
+		return inputType;
+	}
+
+	public void setInputType(String inputType)
+	{
+		this.inputType = inputType;
+	}
+
+	public boolean hasInputPayload()
+	{
+		return (this.inputPayload != null && !this.inputPayload.isEmpty());
+	}
+
+	public String getInputPayload()
+	{
+		return inputPayload;
+	}
+
+	public void setInputPayload(String inputPayload)
+	{
+		this.inputPayload = inputPayload;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [required=" + required + ", desc=" + desc + ", inputType=" + inputType
+				+ ", inputPayload=" + inputPayload + "]";
+	}
+
+	/**
+	 * {@linkplain DataSetParam#getType()}枚举。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class DataType
+	{
+		/** 字符串 */
+		public static final String STRING = "STRING";
+
+		/** 布尔值 */
+		public static final String BOOLEAN = "BOOLEAN";
+
+		/** 整数 */
+		public static final String NUMBER = "NUMBER";
+	}
+
+	/**
+	 * 常用的{@linkplain DataSetParam#getInputType()}枚举。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class InputType
+	{
+		/** 文本框 */
+		public static final String TEXT = "text";
+
+		/** 下拉框 */
+		public static final String SELECT = "select";
+
+		/** 日期 */
+		public static final String DATE = "date";
+
+		/** 时间 */
+		public static final String TIME = "time";
+
+		/** 日期时间 */
+		public static final String DATETIME = "datetime";
+
+		/** 单选框 */
+		public static final String RADIO = "radio";
+
+		/** 复选框 */
+		public static final String CHECKBOX = "checkbox";
+
+		/** 文本域 */
+		public static final String TEXTAREA = "textarea";
+	}
+}

+ 150 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSetProperty.java

@@ -0,0 +1,150 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * 数据集属性信息。
+ * <p>
+ * 此类描述{@linkplain DataSet#getResult(DataSetQuery)}返回的{@linkplain DataSetResult#getData()}元素的属性信息。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetProperty extends AbstractDataNameType implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 展示标签 */
+	private String label = null;
+
+	/** 默认值 */
+	private Object defaultValue = null;
+
+	public DataSetProperty()
+	{
+		super();
+	}
+
+	public DataSetProperty(String name, String type)
+	{
+		super(name, type);
+	}
+
+	public boolean hasLabel()
+	{
+		return (this.label != null && !this.label.isEmpty());
+	}
+
+	public String getLabel()
+	{
+		return label;
+	}
+
+	public void setLabel(String label)
+	{
+		this.label = label;
+	}
+
+	/**
+	 * 获取默认值,可能为{@code null}。
+	 * <p>
+	 * 如果数据中此属性值为{@code null},那么应该设置为此默认值。
+	 * </p>
+	 * <p>
+	 * 注意:此默认值类型不一定是期望的数据类型,应当先对其进行类型转换。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	public Object getDefaultValue()
+	{
+		return defaultValue;
+	}
+
+	public void setDefaultValue(Object defaultValue)
+	{
+		this.defaultValue = defaultValue;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + getName() + ", type=" + getType() + ", label=" + label
+				+ ", defaultValue=" + defaultValue + "]";
+	}
+
+	/**
+	 * {@linkplain DataSetProperty#getType()}类型枚举。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class DataType
+	{
+		/** 字符串 */
+		public static final String STRING = "STRING";
+
+		/** 布尔值 */
+		public static final String BOOLEAN = "BOOLEAN";
+
+		/** 数值,可能是整数或者小数 */
+		public static final String NUMBER = "NUMBER";
+
+		/** 整数 */
+		public static final String INTEGER = "INTEGER";
+
+		/** 小数 */
+		public static final String DECIMAL = "DECIMAL";
+
+		/** 日期 */
+		public static final String DATE = "DATE";
+
+		/** 时间 */
+		public static final String TIME = "TIME";
+
+		/** 时间戳 */
+		public static final String TIMESTAMP = "TIMESTAMP";
+
+		/** 未知类型 */
+		public static final String UNKNOWN = "UNKNOWN";
+
+		/**
+		 * 解析对象的数据类型。
+		 * 
+		 * @param obj
+		 * @return
+		 */
+		public static String resolveDataType(Object obj)
+		{
+			if (obj instanceof String)
+				return STRING;
+			else if (obj instanceof Boolean)
+				return BOOLEAN;
+			else if (obj instanceof Byte || obj instanceof Short || obj instanceof Integer || obj instanceof Long
+					|| obj instanceof BigInteger)
+				return INTEGER;
+			else if (obj instanceof Float || obj instanceof Double || obj instanceof BigDecimal)
+				return DECIMAL;
+			else if (obj instanceof Number)
+				return NUMBER;
+			else if (obj instanceof java.sql.Time)
+				return TIME;
+			else if (obj instanceof java.sql.Timestamp)
+				return TIMESTAMP;
+			else if (obj instanceof java.sql.Date || obj instanceof java.util.Date)
+				return DATE;
+			else
+				return UNKNOWN;
+		}
+	}
+}

+ 236 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSetQuery.java

@@ -0,0 +1,236 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 数据集查询。
+ * <p>
+ * 此类用于从{@linkplain DataSet}中查询{@linkplain DataSetResult}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetQuery implements ResultDataFormatAware, Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 参数值映射表 */
+	private Map<String, Object> paramValues = new HashMap<String, Object>();
+
+	/**结果数据格式*/
+	private ResultDataFormat resultDataFormat = null;
+
+	/** 结果数据最大返回数目 */
+	private int resultFetchSize = -1;
+
+	public DataSetQuery()
+	{
+		super();
+	}
+
+	public DataSetQuery(DataSetQuery query)
+	{
+		super();
+		setParamValues(query.getParamValues());
+		this.resultDataFormat = query.resultDataFormat;
+		this.resultFetchSize = query.resultFetchSize;
+	}
+
+	public Map<String, ?> getParamValues()
+	{
+		return paramValues;
+	}
+
+	/**
+	 * 设置参数值映射表。
+	 * <p>
+	 * 将{@code paramValues}全部放入{@linkplain #getParamValues()}。
+	 * </p>
+	 * <p>
+	 * 参数值映射表的关键字是{@linkplain DataSet#getParam(String)}中的{@linkplain DataSetParam#getName()},应是符合{@linkplain DataSet#isReady(DataSetQuery)}校验的,
+	 * 参数值映射表并不要求与{@linkplain #getParams()}一一对应,通常是包含相同、或者更多的项。
+	 * </p>
+	 * 
+	 * @param paramValues
+	 */
+	public void setParamValues(Map<String, ?> paramValues)
+	{
+		if (paramValues == null)
+			return;
+
+		this.paramValues.putAll(paramValues);
+	}
+	
+	@Override
+	public ResultDataFormat getResultDataFormat()
+	{
+		return resultDataFormat;
+	}
+
+	/**
+	 * 设置结果数据格式。
+	 * <p>
+	 * 当希望自定义{@linkplain DataSet#getResult(Map)}的{@linkplain DataSetResult#getData()}数据格式时,可以设置此项。
+	 * </p>
+	 * 
+	 * @param dataFormat
+	 */
+	@Override
+	public void setResultDataFormat(ResultDataFormat resultDataFormat)
+	{
+		this.resultDataFormat = resultDataFormat;
+	}
+	
+	/**
+	 * 获取结果数据最大返回数目。
+	 * 
+	 * @return {@code <0} 表示不限定数目
+	 */
+	public int getResultFetchSize()
+	{
+		return resultFetchSize;
+	}
+
+	public void setResultFetchSize(int resultFetchSize)
+	{
+		this.resultFetchSize = resultFetchSize;
+	}
+
+	/**
+	 * 设置参数。
+	 * 
+	 * @param name
+	 * @param value
+	 */
+	public void setParamValue(String name, Object value)
+	{
+		this.paramValues.put(name, value);
+	}
+
+	/**
+	 * 获取参数。
+	 * 
+	 * @param <T>
+	 * @param name
+	 * @param value
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> T getParamValue(String name, Object value)
+	{
+		return (T) this.paramValues.get(name);
+	}
+
+	/**
+	 * 删除参数。
+	 * 
+	 * @param <T>
+	 * @param name
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> T removeParamValue(String name)
+	{
+		return (T) this.paramValues.remove(name);
+	}
+
+	/**
+	 * 浅复制此对象。
+	 * 
+	 * @return
+	 */
+	public DataSetQuery copy()
+	{
+		return new DataSetQuery(this);
+	}
+
+	/**
+	 * 构建{@linkplain DataSetQuery}。
+	 * 
+	 * @return
+	 */
+	public static DataSetQuery valueOf()
+	{
+		return new DataSetQuery();
+	}
+	
+	/**
+	 * 构建{@linkplain DataSetQuery}。
+	 * 
+	 * @param paramValues
+	 * @return
+	 */
+	public static DataSetQuery valueOf(Map<String, ?> paramValues)
+	{
+		DataSetQuery query = new DataSetQuery();
+		query.setParamValues(paramValues);
+		
+		return query;
+	}
+	
+	/**
+	 * 构建{@linkplain DataSetQuery}。
+	 * 
+	 * @param paramValues
+	 * @param resultDataFormat
+	 * @return
+	 */
+	public static DataSetQuery valueOf(Map<String, ?> paramValues, ResultDataFormat resultDataFormat)
+	{
+		DataSetQuery query = valueOf(paramValues);
+		query.setResultDataFormat(resultDataFormat);
+		
+		return query;
+	}
+	
+	/**
+	 * 构建{@linkplain DataSetQuery}。
+	 * 
+	 * @param paramValues
+	 * @param resultDataFormat
+	 * @param resultFetchSize
+	 * @return
+	 */
+	public static DataSetQuery valueOf(Map<String, ?> paramValues, ResultDataFormat resultDataFormat, int resultFetchSize)
+	{
+		DataSetQuery query = valueOf(paramValues, resultDataFormat);
+		query.setResultFetchSize(resultFetchSize);
+		
+		return query;
+	}
+	
+	/**
+	 * 构建{@linkplain DataSetQuery}。
+	 * 
+	 * @param query
+	 * @return
+	 */
+	public static DataSetQuery valueOf(DataSetQuery query)
+	{
+		return new DataSetQuery(query);
+	}
+
+	/**
+	 * 拷贝。
+	 * 
+	 * @param query 允许为{@code null}
+	 * @return 如果{@code query}为{@code null},将返回{@code new DataSetQuery()}。
+	 */
+	public static DataSetQuery copy(DataSetQuery query)
+	{
+		if(query == null)
+			return DataSetQuery.valueOf();
+		
+		return query.copy();
+	}
+}

+ 51 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSetResult.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Map;
+
+/**
+ * 数据集结果。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetResult
+{
+	/** 结果数据对象 */
+	private Object data;
+
+	public DataSetResult()
+	{
+		super();
+	}
+
+	public DataSetResult(Object data)
+	{
+		super();
+		this.data = data;
+	}
+
+	/**
+	 * 获取数据对象。
+	 * <p>
+	 * 数据对象应是普通JavaBean、 {@linkplain Map}对象,或者是它们的数组、集合。
+	 * </p>
+	 * 
+	 * @return 数据对象,为{@code null}表示无数据
+	 */
+	public Object getData()
+	{
+		return this.data;
+	}
+
+	public void setData(Object data)
+	{
+		this.data = data;
+	}
+}

+ 102 - 0
datagear-analysis/src/main/java/org/datagear/analysis/DataSign.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.datagear.util.i18n.AbstractLabeled;
+import org.datagear.util.i18n.Labeled;
+
+/**
+ * 数据标记。
+ * <p>
+ * {@linkplain ChartPlugin}使用此类标记{@linkplain DataSet}产生的数据,并依此进行图表绘制。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSign extends AbstractLabeled implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_NAME = "name";
+	public static final String PROPERTY_REQUIRED = "required";
+	public static final String PROPERTY_MULTIPLE = "multiple";
+	public static final String PROPERTY_NAME_LABEL = Labeled.PROPERTY_NAME_LABEL;
+	public static final String PROPERTY_DESC_LABEL = Labeled.PROPERTY_DESC_LABEL;
+
+	/** 名称 */
+	private String name;
+
+	/** 数据集是否必须有此标记 */
+	private boolean required;
+
+	/** 数据集是否可有多个此标记 */
+	private boolean multiple;
+
+	public DataSign()
+	{
+		super();
+	}
+
+	public DataSign(String name, boolean required, boolean multiple)
+	{
+		super();
+		this.name = name;
+		this.required = required;
+		this.multiple = multiple;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public boolean isRequired()
+	{
+		return required;
+	}
+
+	public void setRequired(boolean required)
+	{
+		this.required = required;
+	}
+
+	public boolean isMultiple()
+	{
+		return multiple;
+	}
+
+	public void setMultiple(boolean multiple)
+	{
+		this.multiple = multiple;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + name + ", required=" + required + ", multiple="
+				+ multiple + ", nameLabel=" + getNameLabel() + ", descLabel=" + getDescLabel() + "]";
+	}
+
+	public static List<DataSign> toDataSigns(List<String> labelValues, Locale locale)
+	{
+		List<DataSign> dataSigns = new ArrayList<DataSign>(labelValues.size());
+
+		return dataSigns;
+	}
+}

+ 43 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Icon.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+
+/**
+ * 图标。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface Icon extends Serializable
+{
+	/**
+	 * 获取图标类型:{@code png}、{@code jpeg}等,未知则返回空字符串。
+	 * 
+	 * @return
+	 */
+	String getType();
+
+	/**
+	 * 获取图标输入流。
+	 * 
+	 * @return
+	 * @throws IOException
+	 */
+	InputStream getInputStream() throws IOException;
+
+	/**
+	 * 获取上次修改时间。
+	 * 
+	 * @return
+	 */
+	long getLastModified();
+}

+ 24 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Identifiable.java

@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 可被标识的。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface Identifiable
+{
+	/**
+	 * 获取标识。
+	 * 
+	 * @return
+	 */
+	String getId();
+}

+ 65 - 0
datagear-analysis/src/main/java/org/datagear/analysis/RenderContext.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Map;
+
+/**
+ * 渲染上下文。
+ * <p>
+ * 此类用于定义图表、看板UI渲染上下文。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface RenderContext
+{
+	String PROPERTY_ATTRIBUTES = "attributes";
+
+	/**
+	 * 获取属性。
+	 * 
+	 * @param <T>
+	 * @param name
+	 * @return
+	 */
+	<T> T getAttribute(String name);
+
+	/**
+	 * 设置属性。
+	 * 
+	 * @param name
+	 * @param value
+	 */
+	void setAttribute(String name, Object value);
+
+	/**
+	 * 移除属性。
+	 * 
+	 * @param <T>
+	 * @param name
+	 * @return 已移除的属性值或者{@code null}
+	 */
+	<T> T removeAttribute(String name);
+
+	/**
+	 * 是否有指定属性。
+	 * 
+	 * @param name
+	 * @return
+	 */
+	boolean hasAttribute(String name);
+
+	/**
+	 * 获取所有属性。
+	 * 
+	 * @return
+	 */
+	Map<String, ?> getAttributes();
+}

+ 39 - 0
datagear-analysis/src/main/java/org/datagear/analysis/RenderException.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 渲染异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class RenderException extends RuntimeException
+{
+	private static final long serialVersionUID = 1L;
+
+	public RenderException()
+	{
+		super();
+	}
+
+	public RenderException(String message)
+	{
+		super(message);
+	}
+
+	public RenderException(Throwable cause)
+	{
+		super(cause);
+	}
+
+	public RenderException(String message, Throwable cause)
+	{
+		super(message, cause);
+	}
+}

+ 31 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ResolvableDataSet.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Map;
+
+/**
+ * 可解析{@linkplain DataSetResult}。
+ * <p>
+ * 调用{@linkplain #resolve(Map)}无需预先设置{@linkplain #getProperties()}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface ResolvableDataSet extends DataSet
+{
+	/**
+	 * 解析{@linkplain ResolvedDataSetResult}。
+	 * 
+	 * @param query 应是已通过{@linkplain #isReady(DataSetQuery)}校验的(可能为{@code null})
+	 * @return
+	 * @throws DataSetException
+	 */
+	ResolvedDataSetResult resolve(DataSetQuery query) throws DataSetException;
+}

+ 54 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ResolvedDataSetResult.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.List;
+
+/**
+ * 数据集解析结果。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ResolvedDataSetResult
+{
+	private DataSetResult result;
+
+	private List<DataSetProperty> properties;
+
+	public ResolvedDataSetResult()
+	{
+	}
+
+	public ResolvedDataSetResult(DataSetResult result, List<DataSetProperty> properties)
+	{
+		super();
+		this.result = result;
+		this.properties = properties;
+	}
+
+	public DataSetResult getResult()
+	{
+		return result;
+	}
+
+	public void setResult(DataSetResult result)
+	{
+		this.result = result;
+	}
+
+	public List<DataSetProperty> getProperties()
+	{
+		return properties;
+	}
+
+	public void setProperties(List<DataSetProperty> properties)
+	{
+		this.properties = properties;
+	}
+}

+ 166 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ResultDataFormat.java

@@ -0,0 +1,166 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import org.datagear.util.DateFormat;
+
+/**
+ * 数据集结果数据格式。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ResultDataFormat extends DateFormat
+{
+	private static final long serialVersionUID = 1L;
+	
+	/**
+	 * 格式化类型:数值,表示格式化为数值
+	 */
+	public static final String TYPE_NUMBER = "NUMBER";
+
+	/**
+	 * 格式化类型:字符串,表示格式化为字符串
+	 */
+	public static final String TYPE_STRING = "STRING";
+
+	/**
+	 * 格式化类型:无,表示不格式化,保持原类型
+	 */
+	public static final String TYPE_NONE = "NONE";
+
+	/** 日期格式化类型 */
+	private String dateType = TYPE_STRING;
+
+	/** 时间格式化类型 */
+	private String timeType = TYPE_STRING;
+
+	/** 时间戳格式化类型 */
+	private String timestampType = TYPE_STRING;
+	
+	public ResultDataFormat()
+	{
+		super();
+	}
+
+	public String getDateType()
+	{
+		return dateType;
+	}
+
+	public void setDateType(String dateType)
+	{
+		this.dateType = dateType;
+	}
+
+	/**
+	 * 获取当{@linkplain #getDateType()}为{@linkplain #TYPE_STRING}时的日期格式。
+	 * 
+	 * @return
+	 */
+	@Override
+	public String getDateFormat()
+	{
+		return super.getDateFormat();
+	}
+
+	public String getTimeType()
+	{
+		return timeType;
+	}
+
+	public void setTimeType(String timeType)
+	{
+		this.timeType = timeType;
+	}
+
+	/**
+	 * 获取当{@linkplain #getTimeType()}为{@linkplain #TYPE_STRING}时的时间格式。
+	 * 
+	 * @return
+	 */
+	@Override
+	public String getTimeFormat()
+	{
+		return super.getTimeFormat();
+	}
+
+	public String getTimestampType()
+	{
+		return timestampType;
+	}
+
+	public void setTimestampType(String timestampType)
+	{
+		this.timestampType = timestampType;
+	}
+
+	/**
+	 * 获取当{@linkplain #getTimestampType()}为{@linkplain #TYPE_STRING}时的时间戳格式。
+	 * 
+	 * @return
+	 */
+	@Override
+	public String getTimestampFormat()
+	{
+		return super.getTimestampFormat();
+	}
+
+	@Override
+	public int hashCode()
+	{
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + ((dateType == null) ? 0 : dateType.hashCode());
+		result = prime * result + ((timeType == null) ? 0 : timeType.hashCode());
+		result = prime * result + ((timestampType == null) ? 0 : timestampType.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ResultDataFormat other = (ResultDataFormat) obj;
+		if (dateType == null)
+		{
+			if (other.dateType != null)
+				return false;
+		}
+		else if (!dateType.equals(other.dateType))
+			return false;
+		if (timeType == null)
+		{
+			if (other.timeType != null)
+				return false;
+		}
+		else if (!timeType.equals(other.timeType))
+			return false;
+		if (timestampType == null)
+		{
+			if (other.timestampType != null)
+				return false;
+		}
+		else if (!timestampType.equals(other.timestampType))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [dateType=" + dateType
+				+ ", dateFormat=" + getDateFormat() + ", timeType=" + timeType + ", timeFormat=" + getTimeFormat()
+				+ ", timestampType=" + timestampType + ", timestampFormat=" + getTimestampFormat() + "]";
+	}
+}

+ 31 - 0
datagear-analysis/src/main/java/org/datagear/analysis/ResultDataFormatAware.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * {@linkplain ResultDataFormat}引用类。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface ResultDataFormatAware
+{
+	/**
+	 * 获取{@linkplain ResultDataFormat}。
+	 * 
+	 * @return 返回{@code null}表示没有设置
+	 */
+	ResultDataFormat getResultDataFormat();
+
+	/**
+	 * 设置{@linkplain ResultDataFormat}。
+	 * 
+	 * @param format
+	 */
+	void setResultDataFormat(ResultDataFormat format);
+}

+ 48 - 0
datagear-analysis/src/main/java/org/datagear/analysis/SimpleDashboardQueryHandler.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.util.Map;
+
+/**
+ * 简单{@linkplain DashboardQueryHandler}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class SimpleDashboardQueryHandler extends DashboardQueryHandler
+{
+	private Map<String, ? extends ChartDefinition> chartDefinitions;
+
+	public SimpleDashboardQueryHandler()
+	{
+		super();
+	}
+
+	public SimpleDashboardQueryHandler(Map<String, ? extends ChartDefinition> chartDefinitions)
+	{
+		super();
+		this.chartDefinitions = chartDefinitions;
+	}
+
+	public Map<String, ? extends ChartDefinition> getChartDefinitions()
+	{
+		return chartDefinitions;
+	}
+
+	public void setChartDefinitions(Map<String, ? extends ChartDefinition> chartDefinitions)
+	{
+		this.chartDefinitions = chartDefinitions;
+	}
+
+	@Override
+	protected ChartDefinition getChartDefinition(String chartId)
+	{
+		return (this.chartDefinitions == null ? null : this.chartDefinitions.get(chartId));
+	}
+}

+ 58 - 0
datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboard.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+/**
+ * 模板{@linkplain Dashboard}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class TemplateDashboard extends Dashboard
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 模板 */
+	private String template;
+
+	public TemplateDashboard()
+	{
+		super();
+	}
+
+	public TemplateDashboard(String id, String template, RenderContext renderContext, TemplateDashboardWidget widget)
+	{
+		super(id, renderContext, widget);
+		this.template = template;
+	}
+
+	public String getTemplate()
+	{
+		return template;
+	}
+
+	public void setTemplate(String template)
+	{
+		this.template = template;
+	}
+
+	@Override
+	public TemplateDashboardWidget getWidget()
+	{
+		return (TemplateDashboardWidget) super.getWidget();
+	}
+
+	@Override
+	public void setWidget(DashboardWidget widget)
+	{
+		if (widget != null && !(widget instanceof TemplateDashboardWidget))
+			throw new IllegalArgumentException();
+
+		super.setWidget(widget);
+	}
+}

+ 200 - 0
datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboardWidget.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+package org.datagear.analysis;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 模板看板部件。
+ * <p>
+ * 它可在{@linkplain RenderContext}中渲染其模板名称({@linkplain #getTemplates()})所描述的{@linkplain Dashboard}。
+ * </p>
+ * <p>
+ * 此类的{@linkplain #render(RenderContext)}渲染{@linkplain #getFirstTemplate()}模板。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class TemplateDashboardWidget extends AbstractIdentifiable implements DashboardWidget
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String DEFAULT_TEMPLATE_ENCODING = "UTF-8";
+
+	/** 模板名称集 */
+	private String[] templates;
+
+	private String templateEncoding = DEFAULT_TEMPLATE_ENCODING;
+
+	public TemplateDashboardWidget()
+	{
+		super();
+	}
+
+	public TemplateDashboardWidget(String id, String... templates)
+	{
+		super(id);
+		this.templates = templates;
+	}
+
+	public String[] getTemplates()
+	{
+		return templates;
+	}
+
+	public void setTemplates(String... templates)
+	{
+		this.templates = templates;
+	}
+
+	public String getTemplateEncoding()
+	{
+		return templateEncoding;
+	}
+
+	public void setTemplateEncoding(String templateEncoding)
+	{
+		this.templateEncoding = templateEncoding;
+	}
+
+	/**
+	 * 获取第一个模板名称。
+	 * 
+	 * @return
+	 * @throws IllegalStateException 当没有任何模板时抛出此异常
+	 */
+	public String getFirstTemplate() throws IllegalStateException
+	{
+		if (getTemplateCount() < 1)
+			throw new IllegalStateException();
+
+		return this.templates[0];
+	}
+
+	/**
+	 * 判断是否是模板名称。
+	 * 
+	 * @param template 模板名称
+	 * @return
+	 */
+	public boolean isTemplate(String template)
+	{
+		if (this.templates == null)
+			return false;
+
+		for (String t : this.templates)
+		{
+			if (t.equals(template))
+				return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * 获取模板名称数目。
+	 * 
+	 * @return
+	 */
+	public int getTemplateCount()
+	{
+		return (this.templates == null ? 0 : this.templates.length);
+	}
+
+	/**
+	 * 移除指定模板名称。
+	 * 
+	 * @param template
+	 */
+	public void removeTemplate(String template)
+	{
+		if (this.templates == null || this.templates.length == 0)
+			return;
+
+		List<String> list = new ArrayList<>(this.templates.length);
+
+		for (String t : this.templates)
+		{
+			if (!t.equals(template))
+				list.add(t);
+		}
+
+		this.templates = list.toArray(new String[list.size()]);
+	}
+
+	/**
+	 * 渲染{@linkplain #getFirstTemplate()}的{@linkplain TemplateDashboard}。
+	 */
+	@Override
+	public TemplateDashboard render(RenderContext renderContext) throws RenderException
+	{
+		String template = getFirstTemplate();
+		return renderTemplate(renderContext, template);
+	}
+
+	/**
+	 * 渲染指定模板名称的{@linkplain TemplateDashboard}。
+	 * 
+	 * @param renderContext
+	 * @param template      模板名称,应是{@linkplain #isTemplate(String)}为{@code true}
+	 * @return
+	 * @throws RenderException
+	 * @throws IllegalArgumentException {@code template}不是模板名称时
+	 */
+	public TemplateDashboard render(RenderContext renderContext, String template)
+			throws RenderException, IllegalArgumentException
+	{
+		if (!isTemplate(template))
+			throw new IllegalArgumentException("[" + template + "] is not template");
+
+		return renderTemplate(renderContext, template);
+	}
+
+	/**
+	 * 渲染指定模板名称的{@linkplain TemplateDashboard}。
+	 * <p>
+	 * 模板名称不必是{@linkplain #isTemplate(String)}为{@code true}的,通常用于支持渲染即时看板。
+	 * </p>
+	 * 
+	 * @param renderContext
+	 * @param template      模板名称,{@linkplain #isTemplate(String)}不必为{@code true}
+	 * @param templateIn    {@code template}模板的输入流
+	 * @return
+	 * @throws RenderException
+	 */
+	public TemplateDashboard render(RenderContext renderContext, String template, Reader templateIn)
+			throws RenderException
+	{
+		return renderTemplate(renderContext, template, templateIn);
+	}
+
+	/**
+	 * 渲染指定名称模板。
+	 * 
+	 * @param renderContext
+	 * @param template
+	 * @return
+	 * @throws RenderException
+	 */
+	protected abstract TemplateDashboard renderTemplate(RenderContext renderContext, String template)
+			throws RenderException;
+
+	/**
+	 * 渲染指定名称模板。
+	 * 
+	 * @param renderContext
+	 * @param template
+	 * @param templateIn
+	 * @return
+	 * @throws RenderException
+	 */
+	protected abstract TemplateDashboard renderTemplate(RenderContext renderContext, String template, Reader templateIn)
+			throws RenderException;
+}

+ 193 - 0
datagear-analysis/src/main/java/org/datagear/analysis/TemplateDashboardWidgetResManager.java

@@ -0,0 +1,193 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * {@linkplain TemplateDashboardWidget}资源管理器。
+ * <p>
+ * 此类通过{@linkplain DashboardWidget#getId()}来管理{@linkplain DashboardWidget}资源。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface TemplateDashboardWidgetResManager
+{
+	/**
+	 * 获取默认资源编码。
+	 * 
+	 * @return
+	 */
+	String getDefaultEncoding();
+
+	/**
+	 * 指定名称的资源是否存在。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @return
+	 */
+	boolean exists(String id, String name);
+
+	/**
+	 * 获取指定名称资源的输入流。
+	 * <p>
+	 * 使用{@linkplain TemplateDashboardWidget#getTemplateEncoding()}、或者{@linkplain #getDefaultEncoding()}编码。
+	 * </p>
+	 * 
+	 * @param widget
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @return
+	 * @throws IOException
+	 */
+	Reader getReader(TemplateDashboardWidget widget, String name) throws IOException;
+
+	/**
+	 * 获取指定名称资源的输入流。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @param encoding
+	 *            资源编码,为{@code null}或空则使用默认编码
+	 * @return
+	 * @throws IOException
+	 */
+	Reader getReader(String id, String name, String encoding) throws IOException;
+
+	/**
+	 * 获取指定名称资源的输出流。
+	 * <p>
+	 * 使用{@linkplain TemplateDashboardWidget#getTemplateEncoding()}、或者{@linkplain #getDefaultEncoding()}编码。
+	 * </p>
+	 * 
+	 * @param widget
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @return
+	 * @throws IOException
+	 */
+	Writer getWriter(TemplateDashboardWidget widget, String name) throws IOException;
+
+	/**
+	 * 获取指定名称资源的输出流。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @param encoding
+	 *            资源编码,为{@code null}或空则使用默认编码
+	 * @return
+	 * @throws IOException
+	 */
+	Writer getWriter(String id, String name, String encoding) throws IOException;
+
+	/**
+	 * 获取指定名称资源的输入流。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @return
+	 * @throws IOException
+	 */
+	InputStream getInputStream(String id, String name) throws IOException;
+
+	/**
+	 * 获取指定名称资源的输出流。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 * @return
+	 * @throws IOException
+	 */
+	OutputStream getOutputStream(String id, String name) throws IOException;
+
+	/**
+	 * 将指定目录下的所有文件作为资源拷入。
+	 * <p>
+	 * 拷入后,目录下所有子文件的相对路径名(比如:<code>some-file.txt</code>、<code>some-directory/some-file.png</code>),即可作为此类的资源名称使用。
+	 * </p>
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param directory
+	 * @throws IOException
+	 */
+	void copyFrom(String id, File directory) throws IOException;
+
+	/**
+	 * 将指定{@linkplain TemplateDashboardWidget#getId()}的所有资源拷贝至目标目录。
+	 * 
+	 * @param id
+	 * @param directory
+	 * @throws IOException
+	 */
+	void copyTo(String id, File directory) throws IOException;
+
+	/**
+	 * 将指定源{@linkplain TemplateDashboardWidget#getId()}({@code sourceId})的所有资源拷贝为目标{@linkplain TemplateDashboardWidget#getId()}({@code targetId})资源。
+	 * 
+	 * @param sourceId
+	 * @param targetId
+	 * @throws IOException
+	 */
+	void copyTo(String sourceId, String targetId) throws IOException;
+
+	/**
+	 * 获取指定资源上次修改时间。
+	 * 
+	 * @param id   {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name 模板或者其他资源名称
+	 * @return
+	 */
+	long lastModified(String id, String name);
+
+	/**
+	 * 列出所有资源。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @return
+	 */
+	List<String> list(String id);
+
+	/**
+	 * 删除指定ID的所有资源。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 */
+	void delete(String id);
+
+	/**
+	 * 删除指定资源。
+	 * 
+	 * @param id
+	 *            {@linkplain TemplateDashboardWidget#getId()}
+	 * @param name
+	 *            模板或者其他资源名称
+	 */
+	void delete(String id, String name);
+}

+ 140 - 0
datagear-analysis/src/main/java/org/datagear/analysis/Theme.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis;
+
+import java.io.Serializable;
+
+/**
+ * 主题。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Theme implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 主题名称 */
+	private String name;
+
+	/** 前景色 */
+	private String color;
+
+	/** 背景色 */
+	private String backgroundColor;
+
+	/** 边框颜色 */
+	private String borderColor = "";
+
+	/** 边框宽度 */
+	private String borderWidth = "";
+
+	/** 字体尺寸 */
+	private String fontSize = "";
+
+	public Theme()
+	{
+		super();
+	}
+
+	public Theme(String name, String color, String backgroundColor)
+	{
+		super();
+		this.name = name;
+		this.color = color;
+		this.backgroundColor = backgroundColor;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	public String getColor()
+	{
+		return color;
+	}
+
+	public void setColor(String color)
+	{
+		this.color = color;
+	}
+
+	/**
+	 * 获取背景色。
+	 * 
+	 * @return
+	 */
+	public String getBackgroundColor()
+	{
+		return backgroundColor;
+	}
+
+	public void setBackgroundColor(String backgroundColor)
+	{
+		this.backgroundColor = backgroundColor;
+	}
+
+	public boolean hasBorderColor()
+	{
+		return (this.borderColor != null && !this.borderColor.isEmpty());
+	}
+
+	public String getBorderColor()
+	{
+		return borderColor;
+	}
+
+	public void setBorderColor(String borderColor)
+	{
+		this.borderColor = borderColor;
+	}
+
+	public boolean hasBorderWidth()
+	{
+		return (this.borderWidth != null && !this.borderWidth.isEmpty());
+	}
+
+	public String getBorderWidth()
+	{
+		return borderWidth;
+	}
+
+	public void setBorderWidth(String borderWidth)
+	{
+		this.borderWidth = borderWidth;
+	}
+
+	public boolean hasFontSize()
+	{
+		return (this.fontSize != null && !this.fontSize.isEmpty());
+	}
+
+	public String getFontSize()
+	{
+		return fontSize;
+	}
+
+	public void setFontSize(String fontSize)
+	{
+		this.fontSize = fontSize;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [name=" + name + ", color=" + color + ", backgroundColor="
+				+ backgroundColor + ", borderColor="
+				+ borderColor + ", borderWidth=" + borderWidth + ", fontSize=" + fontSize + "]";
+	}
+}

+ 74 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/AbstractValueConstraint.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 抽象值{@linkplain Constraint}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractValueConstraint<T> implements Constraint
+{
+	private T value;
+
+	public AbstractValueConstraint()
+	{
+	}
+
+	public AbstractValueConstraint(T value)
+	{
+		super();
+		this.value = value;
+	}
+
+	public T getValue()
+	{
+		return value;
+	}
+
+	public void setValue(T value)
+	{
+		this.value = value;
+	}
+
+	@Override
+	public int hashCode()
+	{
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((value == null) ? 0 : value.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		AbstractValueConstraint<?> other = (AbstractValueConstraint<?>) obj;
+		if (value == null)
+		{
+			if (other.value != null)
+				return false;
+		}
+		else if (!value.equals(other.value))
+			return false;
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		return getClass().getSimpleName() + " [value=" + value + "]";
+	}
+}

+ 19 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/Constraint.java

@@ -0,0 +1,19 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface Constraint
+{
+
+}

+ 27 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/Max.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束-最大值。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Max extends AbstractValueConstraint<Number>
+{
+	public Max()
+	{
+		super();
+	}
+
+	public Max(Number value)
+	{
+		super(value);
+	}
+}

+ 27 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/MaxLength.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束-最大长度。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class MaxLength extends AbstractValueConstraint<Integer>
+{
+	public MaxLength()
+	{
+		super();
+	}
+
+	public MaxLength(int value)
+	{
+		super(value);
+	}
+}

+ 27 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/Min.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束-最小值。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Min extends AbstractValueConstraint<Number>
+{
+	public Min()
+	{
+		super();
+	}
+
+	public Min(Number value)
+	{
+		super(value);
+	}
+}

+ 27 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/MinLength.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束-最小长度。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class MinLength extends AbstractValueConstraint<Integer>
+{
+	public MinLength()
+	{
+		super();
+	}
+
+	public MinLength(int value)
+	{
+		super(value);
+	}
+}

+ 27 - 0
datagear-analysis/src/main/java/org/datagear/analysis/constraint/Required.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.constraint;
+
+/**
+ * 约束-必填。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class Required extends AbstractValueConstraint<Boolean>
+{
+	public Required()
+	{
+		super();
+	}
+
+	public Required(boolean value)
+	{
+		super(value);
+	}
+}

+ 200 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractChartPlugin.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.analysis.AbstractIdentifiable;
+import org.datagear.analysis.Category;
+import org.datagear.analysis.ChartParam;
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.analysis.DataSign;
+import org.datagear.analysis.Icon;
+import org.datagear.util.i18n.Label;
+
+/**
+ * 抽象{@linkplain ChartPlugin}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractChartPlugin extends AbstractIdentifiable implements ChartPlugin
+{
+	private static final long serialVersionUID = 1L;
+
+	private Label nameLabel;
+
+	private Label descLabel;
+
+	private Label manualLabel;
+
+	private Map<String, Icon> icons;
+
+	private List<ChartParam> chartParams;
+
+	private List<DataSign> dataSigns;
+
+	private String version;
+
+	private int order = 0;
+
+	private Category category;
+
+	public AbstractChartPlugin()
+	{
+	}
+
+	public AbstractChartPlugin(String id, Label nameLabel)
+	{
+		super(id);
+		this.nameLabel = nameLabel;
+	}
+
+	@Override
+	public Label getNameLabel()
+	{
+		return nameLabel;
+	}
+
+	@Override
+	public void setNameLabel(Label nameLabel)
+	{
+		this.nameLabel = nameLabel;
+	}
+
+	@Override
+	public Label getDescLabel()
+	{
+		return descLabel;
+	}
+
+	@Override
+	public void setDescLabel(Label descLabel)
+	{
+		this.descLabel = descLabel;
+	}
+
+	@Override
+	public Label getManualLabel()
+	{
+		return manualLabel;
+	}
+
+	public void setManualLabel(Label manualLabel)
+	{
+		this.manualLabel = manualLabel;
+	}
+
+	@Override
+	public Map<String, Icon> getIcons()
+	{
+		return icons;
+	}
+
+	public void setIcons(Map<String, Icon> icons)
+	{
+		this.icons = icons;
+	}
+
+	@Override
+	public Icon getIcon(String themeName)
+	{
+		Icon icon = (this.icons == null ? null : this.icons.get(themeName));
+
+		if (icon == null && !DEFAULT_ICON_THEME_NAME.equals(themeName))
+			icon = getIcon(DEFAULT_ICON_THEME_NAME);
+
+		return icon;
+	}
+
+	@Override
+	public List<ChartParam> getChartParams()
+	{
+		return chartParams;
+	}
+
+	public void setChartParams(List<ChartParam> chartParams)
+	{
+		this.chartParams = chartParams;
+	}
+
+	@Override
+	public ChartParam getChartParam(String name)
+	{
+		if (this.chartParams == null)
+			return null;
+
+		for (ChartParam chartParam : this.chartParams)
+		{
+			if (chartParam.getName().equals(name))
+				return chartParam;
+		}
+
+		return null;
+	}
+
+	@Override
+	public List<DataSign> getDataSigns()
+	{
+		return dataSigns;
+	}
+
+	public void setDataSigns(List<DataSign> dataSigns)
+	{
+		this.dataSigns = dataSigns;
+	}
+
+	@Override
+	public DataSign getDataSign(String name)
+	{
+		if (this.dataSigns == null)
+			return null;
+
+		for (DataSign dataSign : this.dataSigns)
+		{
+			if (dataSign.getName().equals(name))
+				return dataSign;
+		}
+
+		return null;
+	}
+
+	@Override
+	public String getVersion()
+	{
+		return version;
+	}
+
+	public void setVersion(String version)
+	{
+		this.version = version;
+	}
+
+	@Override
+	public int getOrder()
+	{
+		return order;
+	}
+
+	public void setOrder(int order)
+	{
+		this.order = order;
+	}
+
+	@Override
+	public Category getCategory()
+	{
+		return category;
+	}
+
+	public void setCategory(Category category)
+	{
+		this.category = category;
+	}
+}

+ 286 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractChartPluginManager.java

@@ -0,0 +1,286 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.analysis.ChartPluginManager;
+import org.datagear.util.StringUtil;
+import org.datagear.util.version.Version;
+
+/**
+ * 抽象{@linkplain ChartPluginManager}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractChartPluginManager implements ChartPluginManager
+{
+	private Map<String, ChartPlugin> chartPluginMap = new HashMap<>();
+
+	public AbstractChartPluginManager()
+	{
+		super();
+	}
+
+	protected Map<String, ChartPlugin> getChartPluginMap()
+	{
+		return chartPluginMap;
+	}
+
+	protected void setChartPluginMap(Map<String, ChartPlugin> chartPluginMap)
+	{
+		this.chartPluginMap = chartPluginMap;
+	}
+
+	/**
+	 * 注册一个{@linkplain ChartPlugin}。
+	 * <p>
+	 * 如果已存在一个更高版本的,则注册失败,返回{@code false}。
+	 * </p>
+	 * 
+	 * @param chartPlugin
+	 * @return
+	 */
+	protected boolean registerChartPlugin(ChartPlugin chartPlugin)
+	{
+		checkLegalChartPlugin(chartPlugin);
+
+		boolean put = true;
+
+		ChartPlugin prev = this.chartPluginMap.get(chartPlugin.getId());
+		if (prev != null)
+			put = canReplaceForSameId(chartPlugin, prev);
+
+		if (put)
+			this.chartPluginMap.put(chartPlugin.getId(), chartPlugin);
+
+		return put;
+	}
+
+	/**
+	 * 移除{@linkplain ChartPlugin}。
+	 * 
+	 * @param id
+	 */
+	protected ChartPlugin[] removeChartPlugins(String[] ids)
+	{
+		ChartPlugin[] removed = new ChartPlugin[ids.length];
+
+		for (int i = 0; i < ids.length; i++)
+			removed[i] = removeChartPlugin(ids[i]);
+
+		return removed;
+	}
+
+	/**
+	 * 移除{@linkplain ChartPlugin}。
+	 * 
+	 * @param id
+	 */
+	protected ChartPlugin removeChartPlugin(String id)
+	{
+		return this.chartPluginMap.remove(id);
+	}
+
+	/**
+	 * 移除所有{@linkplain ChartPlugin}。
+	 */
+	protected void removeAllChartPlugins()
+	{
+		this.chartPluginMap.clear();
+	}
+
+	/**
+	 * 获取指定ID的{@linkplain ChartPlugin}。
+	 * 
+	 * @param id
+	 * @return
+	 */
+	protected ChartPlugin getChartPlugin(String id)
+	{
+		return this.chartPluginMap.get(id);
+	}
+
+	/**
+	 * 查找指定类型的所有{@linkplain ChartPlugin}。
+	 * <p>
+	 * 返回列表已使用{@linkplain #sortChartPlugins(List)}排序。
+	 * </p>
+	 * 
+	 * @param chartPluginType
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	protected <T extends ChartPlugin> List<T> findChartPlugins(Class<? super T> chartPluginType)
+	{
+		List<T> reChartPlugins = new ArrayList<>();
+
+		for (Map.Entry<String, ChartPlugin> entry : this.chartPluginMap.entrySet())
+		{
+			ChartPlugin plugin = entry.getValue();
+
+			if (chartPluginType.isAssignableFrom(plugin.getClass()))
+				reChartPlugins.add((T) plugin);
+		}
+
+		sortChartPlugins(reChartPlugins);
+
+		return reChartPlugins;
+	}
+
+	/**
+	 * 获取所有{@linkplain ChartPlugin}。
+	 * <p>
+	 * 返回列表已使用{@linkplain #sortChartPlugins(List)}排序。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	protected List<ChartPlugin> getAllChartPlugins()
+	{
+		List<ChartPlugin> reChartPlugins = new ArrayList<>();
+
+		reChartPlugins.addAll(this.chartPluginMap.values());
+
+		sortChartPlugins(reChartPlugins);
+
+		return reChartPlugins;
+	}
+
+	protected void sortChartPlugins(List<? extends ChartPlugin> chartPlugins)
+	{
+		sort(chartPlugins);
+	}
+
+	/**
+	 * 按照{@linkplain ChartPlugin#getOrder()}进行排序,越小越靠前。
+	 * 
+	 * @param chartPlugins
+	 */
+	public static void sort(List<? extends ChartPlugin> chartPlugins)
+	{
+		Collections.sort(chartPlugins, new Comparator<ChartPlugin>()
+		{
+			@Override
+			public int compare(ChartPlugin o1, ChartPlugin o2)
+			{
+				return Integer.valueOf(o1.getOrder()).compareTo(o2.getOrder());
+			}
+		});
+	}
+
+	/**
+	 * {@code my}是否可替换{@code old}。
+	 * 
+	 * @param my
+	 * @param old
+	 *            允许为{@code null}
+	 * @return
+	 */
+	protected boolean canReplaceForSameId(ChartPlugin my, ChartPlugin old)
+	{
+		if (old == null)
+			return true;
+
+		boolean replace = false;
+
+		Version myVersion = null;
+		Version oldVersion = null;
+
+		if (!StringUtil.isEmpty(my.getVersion()))
+		{
+			try
+			{
+				myVersion = Version.valueOf(my.getVersion());
+			}
+			catch (Exception e)
+			{
+			}
+		}
+
+		if (!StringUtil.isEmpty(old.getVersion()))
+		{
+			try
+			{
+				oldVersion = Version.valueOf(old.getVersion());
+			}
+			catch (Exception e)
+			{
+			}
+		}
+
+		replace = canReplaceForSameId(my, myVersion, old, oldVersion);
+
+		return replace;
+	}
+
+	/**
+	 * {@code my}是否可替换{@code old}。
+	 * 
+	 * @param my
+	 * @param myVersion
+	 * @param old
+	 * @param oldVersion
+	 * @return
+	 */
+	protected boolean canReplaceForSameId(ChartPlugin my, Version myVersion, ChartPlugin old, Version oldVersion)
+	{
+		boolean replace = false;
+
+		// 没定义版本号的总是替换,便于调试和添加覆盖自定义插件
+		if (StringUtil.isEmpty(oldVersion) && StringUtil.isEmpty(myVersion))
+			replace = true;
+		else if (StringUtil.isEmpty(oldVersion))
+			replace = true;
+		else if (StringUtil.isEmpty(myVersion))
+			replace = false;
+		else
+			replace = myVersion.isHigherThan(oldVersion);
+
+		return replace;
+	}
+
+	/**
+	 * 检查{@linkplain ChartPlugin}是否合法,如果不合法,将抛出{@linkplain IllegalArgumentException}。
+	 * 
+	 * @param chartPlugin
+	 * @throws IllegalArgumentException
+	 */
+	protected void checkLegalChartPlugin(ChartPlugin chartPlugin) throws IllegalArgumentException
+	{
+		if (!isLegalChartPlugin(chartPlugin))
+			throw new IllegalArgumentException("[" + chartPlugin + "] is illegal");
+	}
+
+	/**
+	 * 是否合法的{@linkplain ChartPlugin}。
+	 * 
+	 * @param chartPlugin
+	 * @return
+	 */
+	protected boolean isLegalChartPlugin(ChartPlugin chartPlugin)
+	{
+		if (chartPlugin == null)
+			return false;
+
+		if (StringUtil.isEmpty(chartPlugin.getId()))
+			return false;
+
+		if (chartPlugin.getNameLabel() == null)
+			return false;
+
+		return true;
+	}
+}

+ 395 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractCsvDataSet.java

@@ -0,0 +1,395 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.ResolvableDataSet;
+import org.datagear.analysis.ResolvedDataSetResult;
+import org.datagear.analysis.support.fmk.CsvOutputFormat;
+import org.datagear.util.IOUtil;
+
+/**
+ * 抽象CSV数据集。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractCsvDataSet extends AbstractResolvableDataSet implements ResolvableDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final DataSetFmkTemplateResolver CSV_TEMPLATE_RESOLVER = new DataSetFmkTemplateResolver(
+			CsvOutputFormat.INSTANCE);
+
+	/**
+	 * CSV解析器。
+	 */
+	public static final CSVFormat CSV_FORMAT = CSVFormat.DEFAULT.builder().setIgnoreSurroundingSpaces(true).build();
+
+	/** 作为名称行的行号 */
+	private int nameRow = -1;
+
+	public AbstractCsvDataSet()
+	{
+		super();
+	}
+
+	public AbstractCsvDataSet(String id, String name)
+	{
+		super(id, name);
+	}
+
+	public AbstractCsvDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	/**
+	 * 是否有名称行。
+	 * 
+	 * @return
+	 */
+	public boolean hasNameRow()
+	{
+		return (this.nameRow > 0);
+	}
+
+	/**
+	 * 获取作为名称行的行号。
+	 * 
+	 * @return
+	 */
+	public int getNameRow()
+	{
+		return nameRow;
+	}
+
+	/**
+	 * 设置作为名称行的行号。
+	 * 
+	 * @param nameRow
+	 *            行号,小于{@code 1}则表示无名称行。
+	 */
+	public void setNameRow(int nameRow)
+	{
+		this.nameRow = nameRow;
+	}
+
+	/**
+	 * 解析结果。
+	 * <p>
+	 * 如果{@linkplain #getCsvReader(DataSetQuery)}返回的{@linkplain TemplateResolvedSource#hasResolvedTemplate()},
+	 * 此方法将返回{@linkplain TemplateResolvedDataSetResult}。
+	 * </p>
+	 */
+	@Override
+	protected ResolvedDataSetResult resolveResult(DataSetQuery query, List<DataSetProperty> properties,
+			boolean resolveProperties) throws DataSetException
+	{
+		TemplateResolvedSource<Reader> reader = null;
+
+		try
+		{
+			reader = getCsvReader(query);
+
+			ResolvedDataSetResult result = resolveResult(query, reader.getSource(), properties, resolveProperties);
+
+			if (reader.hasResolvedTemplate())
+				result = new TemplateResolvedDataSetResult(result.getResult(), result.getProperties(),
+						reader.getResolvedTemplate());
+
+			return result;
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t, reader.getResolvedTemplate());
+		}
+		finally
+		{
+			if (reader != null)
+				IOUtil.close(reader.getSource());
+		}
+	}
+
+	/**
+	 * 获取CSV输入流。
+	 * <p>
+	 * 实现方法应该返回实例级不变的输入流。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws Throwable
+	 */
+	protected abstract TemplateResolvedSource<Reader> getCsvReader(DataSetQuery query) throws Throwable;
+
+	/**
+	 * 解析结果。
+	 * 
+	 * @param query
+	 * @param csvReader
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 * @return
+	 * @throws Throwable
+	 */
+	protected ResolvedDataSetResult resolveResult(DataSetQuery query, Reader csvReader,
+			List<DataSetProperty> properties, boolean resolveProperties) throws Throwable
+	{
+		CSVParser csvParser = buildCSVParser(csvReader);
+		List<CSVRecord> csvRecords = csvParser.getRecords();
+
+		List<String> rawDataPropertyNames = resolvePropertyNames(csvRecords);
+		List<Map<String, String>> rawData = resolveRawData(query, rawDataPropertyNames, csvRecords);
+
+		if (resolveProperties)
+		{
+			List<DataSetProperty> resolvedProperties = resolveProperties(rawDataPropertyNames, rawData);
+			mergeDataSetProperties(resolvedProperties, properties);
+			properties = resolvedProperties;
+		}
+
+		return resolveResult(rawData, properties, query.getResultDataFormat());
+	}
+
+	/**
+	 * 解析数据属性名列表。
+	 * 
+	 * @param csvRecords
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<String> resolvePropertyNames(List<CSVRecord> csvRecords) throws Throwable
+	{
+		List<String> propertyNames = null;
+
+		for (int i = 0, len = csvRecords.size(); i < len; i++)
+		{
+			CSVRecord csvRecord = csvRecords.get(i);
+
+			if (isNameRow(i))
+			{
+				int size = csvRecord.size();
+				propertyNames = new ArrayList<String>(csvRecord.size());
+
+				for (int j = 0; j < size; j++)
+					propertyNames.add(csvRecord.get(j));
+
+				break;
+			}
+			else
+			{
+				if (propertyNames == null)
+				{
+					int size = csvRecord.size();
+					propertyNames = new ArrayList<String>(csvRecord.size());
+
+					for (int j = 0; j < size; j++)
+						propertyNames.add(Integer.toString(j + 1));
+				}
+
+				if (isAfterNameRow(i))
+					break;
+			}
+		}
+
+		if (propertyNames == null)
+			propertyNames = Collections.emptyList();
+
+		return propertyNames;
+	}
+
+	/**
+	 * 解析{@linkplain DataSetProperty}。
+	 * 
+	 * @param rawDataPropertyNames
+	 * @param rawData              允许为{@code null}
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<DataSetProperty> resolveProperties(List<String> rawDataPropertyNames,
+			List<Map<String, String>> rawData)
+			throws Throwable
+	{
+		int propertyLen = rawDataPropertyNames.size();
+		List<DataSetProperty> properties = new ArrayList<>(propertyLen);
+
+		for (String name : rawDataPropertyNames)
+			properties.add(new DataSetProperty(name, DataSetProperty.DataType.STRING));
+
+		// 根据数据格式,修订可能的数值类型:只有某一列的所有字符串都是数值格式,才认为是数值类型
+		if (rawData != null && rawData.size() > 0)
+		{
+			boolean[] isNumbers = new boolean[propertyLen];
+			Arrays.fill(isNumbers, true);
+
+			for (Map<String, String> row : rawData)
+			{
+				for (int i = 0; i < propertyLen; i++)
+				{
+					if (!isNumbers[i])
+						continue;
+
+					String value = row.get(rawDataPropertyNames.get(i));
+					isNumbers[i] = isNumberString(value);
+				}
+			}
+
+			for (int i = 0; i < propertyLen; i++)
+			{
+				if (isNumbers[i])
+					properties.get(i).setType(DataSetProperty.DataType.NUMBER);
+			}
+		}
+
+		return properties;
+	}
+
+	/**
+	 * 解析原始数据。
+	 * 
+	 * @param query
+	 * @param propertyNames
+	 * @param csvRecords
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<Map<String, String>> resolveRawData(DataSetQuery query, List<String> propertyNames,
+			List<CSVRecord> csvRecords) throws Throwable
+	{
+		List<Map<String, String>> data = new ArrayList<>();
+
+		for (int i = 0, len = csvRecords.size(); i < len; i++)
+		{
+			if(isNameRow(i))
+				continue;
+
+			if (isReachResultFetchSize(query, data.size()))
+				break;
+
+			Map<String, String> row = new HashMap<>();
+
+			CSVRecord csvRecord = csvRecords.get(i);
+			for (int j = 0, jlen = Math.min(csvRecord.size(), propertyNames.size()); j < jlen; j++)
+			{
+				String name = propertyNames.get(j);
+				String value = csvRecord.get(j);
+
+				row.put(name, value);
+			}
+
+			data.add(row);
+		}
+
+		return data;
+	}
+
+	/**
+	 * 指定的CSV值是否可被当做数值类型。
+	 * 
+	 * @param value
+	 * @return
+	 */
+	protected boolean isNumberString(String value)
+	{
+		if (value == null || value.isEmpty())
+			return false;
+
+		try
+		{
+			parseNumberString(value);
+
+			return true;
+		}
+		catch (Throwable t)
+		{
+			return false;
+		}
+	}
+
+	/**
+	 * 解析数值字符串,{@linkplain #isNumberString(String)}应为{@code true}。
+	 * 
+	 * @param s
+	 * @return
+	 * @throws Throwable
+	 */
+	protected Number parseNumberString(String s) throws Throwable
+	{
+		return Double.parseDouble(s);
+	}
+
+	/**
+	 * 是否名称行
+	 * 
+	 * @param rowIndex
+	 *            行索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isNameRow(int rowIndex)
+	{
+		return ((rowIndex + 1) == this.nameRow);
+	}
+
+	/**
+	 * 是否在名称行之后。
+	 * <p>
+	 * 如果没有名称行,应返回{@code true}。
+	 * </p>
+	 * 
+	 * @param rowIndex
+	 *            行索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isAfterNameRow(int rowIndex)
+	{
+		return ((rowIndex + 1) > this.nameRow);
+	}
+
+	/**
+	 * 构建{@linkplain CSVParser}。
+	 * 
+	 * @param reader
+	 * @return
+	 * @throws Throwable
+	 */
+	protected CSVParser buildCSVParser(Reader reader) throws Throwable
+	{
+		return CSV_FORMAT.parse(reader);
+	}
+
+	/**
+	 * 将指定CSV文本作为模板解析。
+	 * 
+	 * @param csv
+	 * @param query
+	 * @return
+	 */
+	protected String resolveCsvAsTemplate(String csv, DataSetQuery query)
+	{
+		return resolveTextAsTemplate(CSV_TEMPLATE_RESOLVER, csv, query);
+	}
+}

+ 77 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractCsvFileDataSet.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.Reader;
+import java.util.List;
+
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.IOUtil;
+
+/**
+ * 抽象CSV文件数据集。
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractCsvFileDataSet extends AbstractCsvDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 文件编码 */
+	private String encoding = IOUtil.CHARSET_UTF_8;
+
+	public AbstractCsvFileDataSet()
+	{
+		super();
+	}
+
+	public AbstractCsvFileDataSet(String id, String name)
+	{
+		super(id, name);
+	}
+
+	public AbstractCsvFileDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	public String getEncoding()
+	{
+		return encoding;
+	}
+
+	public void setEncoding(String encoding)
+	{
+		this.encoding = encoding;
+	}
+
+	@Override
+	protected TemplateResolvedSource<Reader> getCsvReader(DataSetQuery query) throws Throwable
+	{
+		File file = getCsvFile(query);
+		return new TemplateResolvedSource<>(IOUtil.getReader(file, this.encoding));
+	}
+
+	/**
+	 * 获取CSV文件。
+	 * <p>
+	 * 实现方法应该返回实例级不变的文件。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws Throwable
+	 */
+	protected abstract File getCsvFile(DataSetQuery query) throws Throwable;
+}

+ 573 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java

@@ -0,0 +1,573 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.analysis.AbstractIdentifiable;
+import org.datagear.analysis.DataNameType;
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetParam;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.DataSetResult;
+import org.datagear.analysis.ResolvedDataSetResult;
+import org.datagear.analysis.ResultDataFormat;
+
+/**
+ * 抽象{@linkplain DataSet}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractDataSet extends AbstractIdentifiable implements DataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final DataSetFmkTemplateResolver GENERAL_TEMPLATE_RESOLVER = new DataSetFmkTemplateResolver();
+
+	private String name;
+
+	private boolean mutableModel = false;
+
+	private List<DataSetProperty> properties = Collections.emptyList();
+
+	private List<DataSetParam> params = Collections.emptyList();
+
+	/** 数据格式 */
+	private DataFormat dataFormat = new DataFormat();
+
+	public AbstractDataSet()
+	{
+		super();
+	}
+
+	public AbstractDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id);
+		this.name = name;
+		this.properties = properties;
+	}
+
+	@Override
+	public String getName()
+	{
+		return name;
+	}
+
+	public void setName(String name)
+	{
+		this.name = name;
+	}
+
+	@Override
+	public boolean isMutableModel()
+	{
+		return mutableModel;
+	}
+
+	public void setMutableModel(boolean mutableModel)
+	{
+		this.mutableModel = mutableModel;
+	}
+
+	@Override
+	public List<DataSetProperty> getProperties()
+	{
+		return properties;
+	}
+
+	public void setProperties(List<DataSetProperty> properties)
+	{
+		this.properties = properties;
+	}
+
+	@Override
+	public DataSetProperty getProperty(String name)
+	{
+		return getDataNameTypeByName(this.properties, name);
+	}
+
+	public boolean hasParam()
+	{
+		return (this.params != null && !this.params.isEmpty());
+	}
+
+	@Override
+	public List<DataSetParam> getParams()
+	{
+		return params;
+	}
+
+	public void setParams(List<DataSetParam> params)
+	{
+		this.params = params;
+	}
+
+	@Override
+	public DataSetParam getParam(String name)
+	{
+		return getDataNameTypeByName(this.params, name);
+	}
+
+	/**
+	 * 获取数据格式。
+	 * 
+	 * @return
+	 */
+	public DataFormat getDataFormat()
+	{
+		return dataFormat;
+	}
+
+	/**
+	 * 设置数据格式。
+	 * <p>
+	 * 当数据集属性{@linkplain #getProperties()}的{@linkplain DataSetProperty#getType()}与底层数据源(数据库、CSV、JSON等)不匹配时,
+	 * 可设置此数据格式,用于支持类型转换。
+	 * </p>
+	 * 
+	 * @param dataFormat
+	 */
+	public void setDataFormat(DataFormat dataFormat)
+	{
+		this.dataFormat = dataFormat;
+	}
+
+	/**
+	 * 校验{@linkplain DataSetQuery#getParamValues()}是否有缺失的必填项。
+	 * 
+	 * @param query
+	 * @throws DataSetParamValueRequiredException
+	 */
+	protected void checkRequiredParamValues(DataSetQuery query) throws DataSetParamValueRequiredException
+	{
+		if (!hasParam())
+			return;
+
+		List<DataSetParam> params = getParams();
+		Map<String, ?> paramValues = query.getParamValues();
+		
+		for (DataSetParam param : params)
+		{
+			if (param.isRequired() && !paramValues.containsKey(param.getName()))
+				throw new DataSetParamValueRequiredException(
+						"Parameter [" + param.getName() + "] 's value is required");
+		}
+	}
+	
+	/**
+	 * 解析结果数据。
+	 * 
+	 * @param rawData    {@code Collection<Map<String, ?>>}、{@code Map<String, ?>[]}、{@code Map<String, ?>}、{@code null}
+	 * @param properties
+	 * @param format     允许为{@code null}
+	 * @return {@code List<Map<String, ?>>}、{@code Map<String, ?>[]}、{@code Map<String, ?>}、{@code null}
+	 * @throws Throwable
+	 */
+	@SuppressWarnings({ "unchecked", "rawtypes" })
+	protected Object resolveResultData(Object rawData, List<DataSetProperty> properties,
+			ResultDataFormat format) throws Throwable
+	{
+		Object data = null;
+
+		if (rawData == null)
+		{
+
+		}
+		else if (rawData instanceof Collection<?>)
+		{
+			Collection<Map<String, ?>> rawCollection = (Collection<Map<String, ?>>) rawData;
+
+			data = convertRawDataToResult(rawCollection, properties, format);
+		}
+		else if (rawData instanceof Map<?, ?>[])
+		{
+			Map<?, ?>[] rawArray = (Map<?, ?>[]) rawData;
+			List<Map<String, ?>> rawCollection = (List) Arrays.asList(rawArray);
+			List<Map<String, Object>> dataList = convertRawDataToResult(rawCollection, properties, format);
+
+			data = dataList.toArray(new Map<?, ?>[dataList.size()]);
+		}
+		else if (rawData instanceof Map<?, ?>)
+		{
+			Map<?, ?> rawMap = (Map<?, ?>) rawData;
+			List<Map<String, ?>> rawCollection = (List) Arrays.asList(rawMap);
+			List<Map<String, Object>> dataList = convertRawDataToResult(rawCollection, properties, format);
+
+			data = dataList.get(0);
+		}
+		else
+			throw new UnsupportedOperationException(
+					"Unsupported raw data type : " + rawData.getClass().getSimpleName());
+
+		return data;
+	}
+
+	/**
+	 * 解析结果。
+	 * 
+	 * @param rawData
+	 * @param properties
+	 * @param format     允许为{@code null}
+	 * @return
+	 * @throws Throwable
+	 * @see {@link #resolveResultData(Object, List, ResultDataFormat)}
+	 */
+	protected ResolvedDataSetResult resolveResult(Object rawData, List<DataSetProperty> properties,
+			ResultDataFormat format) throws Throwable
+	{
+		Object data = resolveResultData(rawData, properties, format);
+		return new ResolvedDataSetResult(new DataSetResult(data), properties);
+	}
+
+	/**
+	 * 转换原始数据。
+	 * 
+	 * @param rawData
+	 * @param properties
+	 * @param format 允许为{@code null}
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<Map<String, Object>> convertRawDataToResult(Collection<? extends Map<String, ?>> rawData,
+			List<DataSetProperty> properties, ResultDataFormat format) throws Throwable
+	{
+		DataSetPropertyValueConverter converter = createDataSetPropertyValueConverter();
+		ResultDataFormatter formatter = (format == null ? null : new ResultDataFormatter(format));
+
+		List<Map<String, Object>> data = new ArrayList<>(rawData.size());
+
+		int plen = properties.size();
+
+		Object[] defaultValues = new Object[plen];
+		Object dvPlaceholder = new Object();
+		Arrays.fill(defaultValues, dvPlaceholder);
+
+		for (Map<String, ?> rowRaw : rawData)
+		{
+			// 易变模型应保留所有原始数据
+			Map<String, Object> row = (isMutableModel() ? new HashMap<>(rowRaw) : new HashMap<>());
+
+			for (int j = 0; j < plen; j++)
+			{
+				DataSetProperty property = properties.get(j);
+
+				String name = property.getName();
+				Object value = rowRaw.get(name);
+				value = convertToPropertyDataType(converter, value, property);
+
+				if (value == null)
+				{
+					if (defaultValues[j] == dvPlaceholder)
+					{
+						Object defaultValue = property.getDefaultValue();
+						defaultValues[j] = convertToPropertyDataType(converter, defaultValue, property);
+					}
+
+					value = defaultValues[j];
+				}
+
+				if (formatter != null)
+					value = formatter.format(value);
+
+				row.put(name, value);
+			}
+
+			data.add(row);
+		}
+
+		return data;
+	}
+
+	/**
+	 * 是否有{@linkplain DataSetQuery#getResultFetchSize()}。
+	 * 
+	 * @param query 允许为{@code null}
+	 * @return
+	 */
+	protected boolean hasResultFetchSize(DataSetQuery query)
+	{
+		if (query == null)
+			return false;
+
+		int maxCount = query.getResultFetchSize();
+
+		if (maxCount < 0)
+			return false;
+
+		return true;
+	}
+
+	/**
+	 * 给定数目是否已到达{@linkplain DataSetQuery#getResultFetchSize()}。
+	 * 
+	 * @param query
+	 *            允许为{@code null}
+	 * @param count
+	 * @return
+	 */
+	protected boolean isReachResultFetchSize(DataSetQuery query, int count)
+	{
+		if (query == null)
+			return false;
+
+		int maxCount = query.getResultFetchSize();
+
+		if (maxCount < 0)
+			return false;
+
+		return count >= maxCount;
+	}
+
+	/**
+	 * 计算结果数据最大数目。
+	 * 
+	 * @param query
+	 * @param defaultSize
+	 * @return
+	 */
+	protected int evalResultFetchSize(DataSetQuery dataSetOption, int defaultSize)
+	{
+		if (dataSetOption == null)
+			return defaultSize;
+
+		int maxCount = dataSetOption.getResultFetchSize();
+
+		return (maxCount < 0 ? defaultSize : Math.min(maxCount, defaultSize));
+	}
+	
+	/**
+	 * 查找与名称数组对应的{@linkplain DataSetProperty}列表。
+	 * <p>
+	 * 如果{@code names}某元素没有对应的{@linkplain DataSetProperty},返回列表对应元素位置将为{@code null}。
+	 * </p>
+	 * 
+	 * @param dataSetProperties
+	 * @param names
+	 * @return
+	 */
+	protected List<DataSetProperty> findDataSetProperties(List<DataSetProperty> dataSetProperties, String[] names)
+	{
+		return findDataSetProperties(dataSetProperties, Arrays.asList(names));
+	}
+
+	/**
+	 * 查找与名称数组对应的{@linkplain DataSetProperty}列表。
+	 * <p>
+	 * 如果{@code names}某元素没有对应的{@linkplain DataSetProperty},返回列表对应元素位置将为{@code null}。
+	 * </p>
+	 * 
+	 * @param dataSetProperties
+	 * @param names
+	 * @return
+	 */
+	protected List<DataSetProperty> findDataSetProperties(List<DataSetProperty> dataSetProperties, List<String> names)
+	{
+		List<DataSetProperty> re = new ArrayList<>(names.size());
+
+		for (int i = 0, len = names.size(); i < len; i++)
+		{
+			DataSetProperty dp = null;
+
+			for (DataSetProperty dataSetProperty : dataSetProperties)
+			{
+				if (names.get(i).equals(dataSetProperty.getName()))
+				{
+					dp = dataSetProperty;
+					break;
+				}
+			}
+
+			re.add(dp);
+		}
+
+		return re;
+	}
+
+	/**
+	 * 将源对象转换为指定{@linkplain DataSetProperty.DataType}类型的对象。
+	 * <p>
+	 * 如果{@code property}为{@code null},则什么也不做直接返回。
+	 * </p>
+	 * 
+	 * @param converter
+	 * @param source
+	 *            允许为{@code null}
+	 * @param property
+	 *            允许为{@code null}
+	 * @return
+	 */
+	protected Object convertToPropertyDataType(DataSetPropertyValueConverter converter, Object source,
+			DataSetProperty property)
+	{
+		if (property == null)
+			return source;
+
+		if (source == null)
+			return null;
+
+		return convertToPropertyDataType(converter, source, property.getType());
+	}
+
+	/**
+	 * 将源对象转换为指定{@linkplain DataSetProperty.DataType}类型的对象。
+	 * <p>
+	 * 如果{@code propertyType}为{@code null},则什么也不做直接返回。
+	 * </p>
+	 * 
+	 * @param converter
+	 * @param source
+	 * @param propertyType
+	 *            允许为{@code null}
+	 * @return
+	 */
+	protected Object convertToPropertyDataType(DataSetPropertyValueConverter converter, Object source,
+			String propertyType)
+	{
+		if (propertyType == null || DataSetProperty.DataType.UNKNOWN.equals(propertyType))
+			return source;
+
+		return converter.convert(source, propertyType);
+	}
+
+	/**
+	 * 创建一个{@linkplain DataSetPropertyValueConverter}实例。
+	 * <p>
+	 * 由于{@linkplain DataSetPropertyValueConverter}不是线程安全的,所以每次使用时要手动创建。
+	 * </p>
+	 * 
+	 * @return
+	 */
+	protected DataSetPropertyValueConverter createDataSetPropertyValueConverter()
+	{
+		DataFormat dataFormat = getDataFormat();
+		if (dataFormat == null)
+			dataFormat = new DataFormat();
+
+		return new DataSetPropertyValueConverter(dataFormat);
+	}
+
+	/**
+	 * 解析{@linkplain DataSetProperty.DataType}类型。
+	 * 
+	 * @param value
+	 * @return
+	 */
+	protected String resolvePropertyDataType(Object value)
+	{
+		return DataSetProperty.DataType.resolveDataType(value);
+	}
+
+	/**
+	 * 获取指定名称的{@linkplain DataNameType}对象,没找到则返回{@code null}。
+	 * 
+	 * @param <T>
+	 * @param list
+	 *            允许为{@code null}
+	 * @param name
+	 * @return
+	 */
+	protected <T extends DataNameType> T getDataNameTypeByName(List<T> list, String name)
+	{
+		int index = getDataNameTypeIndexByName(list, name);
+		return (index < 0 ? null : list.get(index));
+	}
+
+	/**
+	 * 获取指定名称的{@linkplain DataNameType}的索引。
+	 * 
+	 * @param <T>
+	 * @param list
+	 *            允许为{@code null}
+	 * @param name
+	 * @return
+	 */
+	protected <T extends DataNameType> int getDataNameTypeIndexByName(List<T> list, String name)
+	{
+		if (list == null)
+			list = Collections.emptyList();
+
+		for (int i = 0, len = list.size(); i < len; i++)
+		{
+			if (name.equals(list.get(i).getName()))
+				return i;
+		}
+
+		return -1;
+	}
+
+	@SuppressWarnings("unchecked")
+	protected List<Map<String, Object>> listRowsToMapRows(List<List<Object>> data, List<DataSetProperty> properties)
+	{
+		if (data == null)
+			return Collections.EMPTY_LIST;
+
+		int plen = properties.size();
+
+		List<Map<String, Object>> maps = new ArrayList<>(data.size());
+
+		for (List<Object> row : data)
+		{
+			Map<String, Object> map = new HashMap<>();
+
+			for (int i = 0; i < Math.min(plen, row.size()); i++)
+			{
+				String name = properties.get(i).getName();
+				map.put(name, row.get(i));
+			}
+
+			maps.add(map);
+		}
+
+		return maps;
+	}
+
+	/**
+	 * 将指定文本作为通用模板解析(没有特定语境,比如:SQL、CSV、JSON)。
+	 * 
+	 * @param text
+	 * @param query
+	 * @return
+	 */
+	protected String resolveTextAsGeneralTemplate(String text, DataSetQuery query)
+	{
+		return resolveTextAsTemplate(GENERAL_TEMPLATE_RESOLVER, text, query);
+	}
+
+	/**
+	 * 将指定文本作为模板解析。
+	 * <p>
+	 * 注意:即使此数据集没有定义任何参数({@linkplain #hasParam()}为{@code false}),此方法也必须将{@code text}作为模板解析,因为存在如下应用场景:
+	 * </p>
+	 * <ol>
+	 * <li>用户编写了无需参数的模板内容;</li>
+	 * <li>用户不定义数据集参数,但却定义模板内容,之后用户自行在{@linkplain DataSet#getResult(DataSetQuery)}参数映射表中传递模板内容所须的参数值;</li>
+	 * </ol>
+	 * 
+	 * @param templateResolver
+	 * @param text
+	 * @param query
+	 * @return
+	 */
+	protected String resolveTextAsTemplate(TemplateResolver templateResolver, String text, DataSetQuery query)
+	{
+		if (text == null)
+			return null;
+
+		Map<String, ?> values = query.getParamValues();
+
+		return templateResolver.resolve(text, new TemplateContext(values));
+	}
+}

+ 770 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java

@@ -0,0 +1,770 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.opc.PackageAccess;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.ResolvableDataSet;
+import org.datagear.analysis.ResolvedDataSetResult;
+import org.datagear.analysis.support.RangeExpResolver.IndexRange;
+import org.datagear.analysis.support.RangeExpResolver.Range;
+import org.datagear.util.FileUtil;
+import org.datagear.util.IOUtil;
+import org.datagear.util.StringUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 抽象Excel数据集。
+ * <p>
+ * 此类仅支持从Excel的单个sheet读取数据,具体参考{@linkplain #setSheetIndex(int)}。
+ * </p>
+ * <p>
+ * 通过{@linkplain #setDataRowExp(String)}、{@linkplain #setDataColumnExp(String)}来设置读取行、列范围。
+ * </p>
+ * <p>
+ * 通过{@linkplain #setNameRow(int)}可设置名称行。
+ * </p>
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractExcelDataSet extends AbstractResolvableDataSet implements ResolvableDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelDataSet.class);
+
+	public static final String EXTENSION_XLSX = "xlsx";
+
+	public static final String EXTENSION_XLS = "xls";
+
+	protected static final RangeExpResolver RANGE_EXP_RESOLVER = RangeExpResolver
+			.valueOf(RangeExpResolver.RANGE_SPLITTER_CHAR, RangeExpResolver.RANGE_GROUP_SPLITTER_CHAR);
+
+	/** 此数据集所处的sheet索引号(以1计数) */
+	private int sheetIndex = 1;
+
+	/** 作为名称行的行号 */
+	private int nameRow = -1;
+
+	/** 数据行范围表达式 */
+	private String dataRowExp = "";
+
+	/** 数据列范围表达式 */
+	private String dataColumnExp = "";
+
+	/** 是否强制作为xls文件处理 */
+	private boolean forceXls = false;
+
+	private transient List<IndexRange> _dataRowRanges = null;
+	private transient List<IndexRange> _dataColumnRanges = null;
+
+	public AbstractExcelDataSet()
+	{
+		super();
+	}
+
+	public AbstractExcelDataSet(String id, String name)
+	{
+		super(id, name);
+	}
+
+	public AbstractExcelDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	public int getSheetIndex()
+	{
+		return sheetIndex;
+	}
+
+	/**
+	 * 设置此数据集所处的sheet号。
+	 * 
+	 * @param sheetIndex
+	 *            sheet号(以{@code 1}计数)
+	 */
+	public void setSheetIndex(int sheetIndex)
+	{
+		this.sheetIndex = sheetIndex;
+	}
+
+	/**
+	 * 是否有名称行。
+	 * 
+	 * @return
+	 */
+	public boolean hasNameRow()
+	{
+		return (this.nameRow > 0);
+	}
+
+	/**
+	 * 获取作为名称行的行号。
+	 * 
+	 * @return
+	 */
+	public int getNameRow()
+	{
+		return nameRow;
+	}
+
+	/**
+	 * 设置作为名称行的行号。
+	 * 
+	 * @param nameRow
+	 *            行号,小于{@code 1}则表示无名称行。
+	 */
+	public void setNameRow(int nameRow)
+	{
+		this.nameRow = nameRow;
+	}
+
+	public String getDataRowExp()
+	{
+		return dataRowExp;
+	}
+
+	/**
+	 * 设置数据行范围表达式。
+	 * <p>
+	 * 表达式格式示例为:
+	 * </p>
+	 * <p>
+	 * {@code "6"} :第6行 <br>
+	 * {@code "3-15"} :第3至15行 <br>
+	 * {@code "1,4,8-15"}:第1、4、8至15行
+	 * </p>
+	 * <p>
+	 * 标题行({@linkplain #getNameRow()})将自动被排除。
+	 * </p>
+	 * <p>
+	 * 注意:行号以{@code 1}开始计数。
+	 * </p>
+	 * 
+	 * @param dataRowExp
+	 *            表达式,为{@code null}、{@code ""}则不限定
+	 */
+	public void setDataRowExp(String dataRowExp)
+	{
+		this.dataRowExp = dataRowExp;
+		this._dataRowRanges = getRangeExpResolver().resolveIndex(this.dataRowExp);
+	}
+
+	public String getDataColumnExp()
+	{
+		return dataColumnExp;
+	}
+
+	/**
+	 * 设置数据列范围表达式。
+	 * <p>
+	 * 表达式格式为:
+	 * </p>
+	 * <p>
+	 * {@code "A"}:第A列 <br>
+	 * {@code "C-E"}:第C至E列 <br>
+	 * {@code "A,C,E-H"}:第A、C、E至H列
+	 * </p>
+	 * 
+	 * @param dataColumnExp
+	 *            表达式,为{@code null}、{@code ""}则不限定
+	 */
+	public void setDataColumnExp(String dataColumnExp)
+	{
+		this.dataColumnExp = dataColumnExp;
+		this._dataColumnRanges = resolveDataColumnRanges(dataColumnExp);
+	}
+
+	/**
+	 * 是否强制作为xls文件处理。
+	 * 
+	 * @return
+	 */
+	public boolean isForceXls()
+	{
+		return forceXls;
+	}
+
+	/**
+	 * 设置是否强制作为xls文件处理,如果为{@code false},则根据文件扩展名判断。
+	 * 
+	 * @param forceXls
+	 */
+	public void setForceXls(boolean forceXls)
+	{
+		this.forceXls = forceXls;
+	}
+
+	@Override
+	protected ResolvedDataSetResult resolveResult(DataSetQuery query, List<DataSetProperty> properties,
+			boolean resolveProperties) throws DataSetException
+	{
+		File file = null;
+
+		try
+		{
+			file = getExcelFile(query);
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t);
+		}
+
+		ResolvedDataSetResult result = null;
+
+		if (isXls(file))
+			result = resolveResultForXls(query, file, properties, resolveProperties);
+		else
+			result = resolveResultForXlsx(query, file, properties, resolveProperties);
+
+		return result;
+	}
+
+	/**
+	 * 解析{@code xls}结果。
+	 * 
+	 * @param query
+	 * @param file
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 * @throws DataSetException
+	 */
+	protected ResolvedDataSetResult resolveResultForXls(DataSetQuery query, File file,
+			List<DataSetProperty> properties, boolean resolveProperties) throws DataSetException
+	{
+		POIFSFileSystem poifs = null;
+		HSSFWorkbook wb = null;
+
+		try
+		{
+			poifs = new POIFSFileSystem(file, true);
+			wb = new HSSFWorkbook(poifs.getRoot(), true);
+
+			Sheet sheet = wb.getSheetAt(getSheetIndex() - 1);
+
+			return resolveResultForSheet(query, sheet, properties, resolveProperties);
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t);
+		}
+		finally
+		{
+			IOUtil.close(wb);
+			IOUtil.close(poifs);
+		}
+	}
+
+	/**
+	 * 解析{@code xlsx}结果。
+	 * 
+	 * @param query
+	 * @param file
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 * @return
+	 * @throws DataSetException
+	 */
+	protected ResolvedDataSetResult resolveResultForXlsx(DataSetQuery query, File file,
+			List<DataSetProperty> properties, boolean resolveProperties) throws DataSetException
+	{
+		OPCPackage pkg = null;
+		XSSFWorkbook wb = null;
+
+		try
+		{
+			pkg = OPCPackage.open(file, PackageAccess.READ);
+			wb = new XSSFWorkbook(pkg);
+
+			Sheet sheet = wb.getSheetAt(getSheetIndex() - 1);
+
+			return resolveResultForSheet(query, sheet, properties, resolveProperties);
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t);
+		}
+		finally
+		{
+			IOUtil.close(wb);
+			IOUtil.close(pkg);
+		}
+	}
+
+	/**
+	 * 解析sheet结果。
+	 * 
+	 * @param query
+	 * @param sheet
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 * @return
+	 * @throws Throwable
+	 */
+	protected ResolvedDataSetResult resolveResultForSheet(DataSetQuery query, Sheet sheet,
+			List<DataSetProperty> properties, boolean resolveProperties) throws Throwable
+	{
+		List<Row> excelRows = new ArrayList<Row>();
+
+		for (Row row : sheet)
+			excelRows.add(row);
+
+		List<ExcelPropertyInfo> rawDataPropertyInfos = resolvePropertyInfos(excelRows);
+		List<Map<String, Object>> rawData = resolveRawData(query, rawDataPropertyInfos, excelRows);
+
+		if (resolveProperties)
+		{
+			List<String> rawDataPropertyNames = toPropertyNames(rawDataPropertyInfos);
+			List<DataSetProperty> resolvedProperties = resolveProperties(rawDataPropertyNames, rawData);
+			mergeDataSetProperties(resolvedProperties, properties);
+			properties = resolvedProperties;
+		}
+
+		return resolveResult(rawData, properties, query.getResultDataFormat());
+	}
+
+	/**
+	 * 解析数据属性信息列表。
+	 * 
+	 * @param excelRows
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<ExcelPropertyInfo> resolvePropertyInfos(List<Row> excelRows) throws Throwable
+	{
+		List<ExcelPropertyInfo> propertyInfos = null;
+
+		for (int i = 0, len = excelRows.size(); i < len; i++)
+		{
+			Row row = excelRows.get(i);
+
+			if (isNameRow(i))
+			{
+				propertyInfos = new ArrayList<ExcelPropertyInfo>();
+
+				short minColIdx = row.getFirstCellNum(), maxColIdx = row.getLastCellNum();
+				for (short colIdx = minColIdx; colIdx < maxColIdx; colIdx++)
+				{
+					if (isDataColumn(colIdx))
+					{
+						String name = null;
+
+						Cell cell = row.getCell(colIdx);
+
+						if (cell != null)
+						{
+							try
+							{
+								name = cell.getStringCellValue();
+							}
+							catch(Throwable t)
+							{
+							}
+						}
+
+						if (StringUtil.isEmpty(name))
+							name = CellReference.convertNumToColString(colIdx);
+
+						propertyInfos.add(new ExcelPropertyInfo(name, colIdx));
+					}
+				}
+
+				break;
+			}
+			else if (isDataRow(i))
+			{
+				if (propertyInfos == null)
+				{
+					propertyInfos = new ArrayList<ExcelPropertyInfo>();
+
+					short minColIdx = row.getFirstCellNum(), maxColIdx = row.getLastCellNum();
+					for (short colIdx = minColIdx; colIdx < maxColIdx; colIdx++)
+					{
+						if (isDataColumn(colIdx))
+						{
+							String name = CellReference.convertNumToColString(colIdx);
+							propertyInfos.add(new ExcelPropertyInfo(name, colIdx));
+						}
+					}
+				}
+
+				if (isAfterNameRow(i))
+					break;
+			}
+		}
+
+		if (propertyInfos == null)
+			propertyInfos = Collections.emptyList();
+
+		return propertyInfos;
+	}
+
+	/**
+	 * 解析{@linkplain DataSetProperty}。
+	 * 
+	 * @param rawDataPropertyNames
+	 * @param rawData              允许为{@code null}
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<DataSetProperty> resolveProperties(List<String> rawDataPropertyNames,
+			List<Map<String, Object>> rawData) throws Throwable
+	{
+		int propertyLen = rawDataPropertyNames.size();
+		List<DataSetProperty> properties = new ArrayList<>(propertyLen);
+
+		for (String name : rawDataPropertyNames)
+			properties.add(new DataSetProperty(name, DataSetProperty.DataType.UNKNOWN));
+
+		if (rawData != null && rawData.size() > 0)
+		{
+			for (Map<String, Object> row : rawData)
+			{
+				int resolvedPropertyTypeCount = 0;
+
+				for (int i = 0; i < propertyLen; i++)
+				{
+					DataSetProperty property = properties.get(i);
+
+					if (!DataSetProperty.DataType.UNKNOWN.equals(property.getType()))
+					{
+						resolvedPropertyTypeCount++;
+						continue;
+					}
+
+					Object value = row.get(rawDataPropertyNames.get(i));
+
+					if (value != null)
+						property.setType(resolvePropertyDataType(value));
+				}
+
+				if (resolvedPropertyTypeCount == propertyLen)
+					break;
+			}
+		}
+
+		return properties;
+	}
+
+	/**
+	 * 解析原始数据。
+	 * 
+	 * @param query
+	 * @param propertyInfos
+	 * @param excelRows
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<Map<String, Object>> resolveRawData(DataSetQuery query,
+			List<ExcelPropertyInfo> propertyInfos, List<Row> excelRows) throws Throwable
+	{
+		List<Map<String, Object>> data = new ArrayList<>();
+
+		Map<Short, String> cellNumPropertyNames = toCellNumPropertyNames(propertyInfos);
+
+		for (int i = 0, len = excelRows.size(); i < len; i++)
+		{
+			if (isNameRow(i) || !isDataRow(i))
+				continue;
+
+			if (isReachResultFetchSize(query, data.size()))
+				break;
+
+			Map<String, Object> row = new HashMap<>();
+
+			Row excelRow = excelRows.get(i);
+
+			short minColIdx = excelRow.getFirstCellNum(), maxColIdx = excelRow.getLastCellNum();
+			for (short colIdx = minColIdx; colIdx < maxColIdx; colIdx++)
+			{
+				if (isDataColumn(colIdx))
+				{
+					Cell cell = excelRow.getCell(colIdx);
+					String name = cellNumPropertyNames.get(colIdx);
+					Object value = resolveCellValue(cell);
+					row.put(name, value);
+				}
+			}
+
+			data.add(row);
+		}
+
+		return data;
+	}
+
+	protected Map<Short, String> toCellNumPropertyNames(List<ExcelPropertyInfo> propertyInfos)
+	{
+		Map<Short, String> re = new HashMap<Short, String>();
+
+		for (ExcelPropertyInfo epi : propertyInfos)
+			re.put(epi.getCellIdx(), epi.getName());
+
+		return re;
+	}
+
+	protected List<String> toPropertyNames(List<ExcelPropertyInfo> propertyInfos)
+	{
+		List<String> re = new ArrayList<String>(propertyInfos.size());
+
+		for (ExcelPropertyInfo epi : propertyInfos)
+			re.add(epi.getName());
+
+		return re;
+	}
+
+	/**
+	 * 解析单元格属性值。
+	 * 
+	 * @param cell 允许为{@code null}
+	 * @return
+	 * @throws DataSetSourceParseException
+	 * @throws DataSetException
+	 */
+	protected Object resolveCellValue(Cell cell) throws DataSetSourceParseException, DataSetException
+	{
+		if (cell == null)
+			return null;
+
+		CellType cellType = cell.getCellType();
+
+		Object cellValue = null;
+
+		try
+		{
+			if (CellType.BLANK.equals(cellType))
+			{
+				cellValue = null;
+			}
+			else if (CellType.BOOLEAN.equals(cellType))
+			{
+				cellValue = cell.getBooleanCellValue();
+			}
+			else if (CellType.ERROR.equals(cellType))
+			{
+				cellValue = cell.getErrorCellValue();
+			}
+			else if (CellType.FORMULA.equals(cellType))
+			{
+				cellValue = cell.getCellFormula();
+			}
+			else if (CellType.NUMERIC.equals(cellType))
+			{
+				if (DateUtil.isCellDateFormatted(cell))
+					cellValue = cell.getDateCellValue();
+				else
+					cellValue = cell.getNumericCellValue();
+			}
+			else if (CellType.STRING.equals(cellType))
+			{
+				cellValue = cell.getStringCellValue();
+			}
+		}
+		catch(DataSetException e)
+		{
+			throw e;
+		}
+		catch(Throwable t)
+		{
+			throw new DataSetSourceParseException(t);
+		}
+
+		return cellValue;
+	}
+
+	/**
+	 * 是否名称行
+	 * 
+	 * @param rowIndex
+	 *            行索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isNameRow(int rowIndex)
+	{
+		return ((rowIndex + 1) == this.nameRow);
+	}
+
+	/**
+	 * 是否在名称行之后。
+	 * <p>
+	 * 如果没有名称行,应返回{@code true}。
+	 * </p>
+	 * 
+	 * @param rowIndex
+	 *            行索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isAfterNameRow(int rowIndex)
+	{
+		return ((rowIndex + 1) > this.nameRow);
+	}
+
+	/**
+	 * 是否数据行。
+	 * 
+	 * @param rowIndex
+	 *            行索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isDataRow(int rowIndex)
+	{
+		if (isNameRow(rowIndex))
+			return false;
+
+		if (this._dataRowRanges == null || this._dataRowRanges.isEmpty())
+			return true;
+
+		return IndexRange.includes(this._dataRowRanges, rowIndex + 1);
+	}
+
+	/**
+	 * 是否数据列。
+	 * 
+	 * @param columnIndex
+	 *            列索引(以{@code 0}计数)
+	 * @return
+	 */
+	protected boolean isDataColumn(int columnIndex)
+	{
+		if (this._dataColumnRanges == null || this._dataColumnRanges.isEmpty())
+			return true;
+
+		return IndexRange.includes(this._dataColumnRanges, columnIndex);
+	}
+
+	@SuppressWarnings("unchecked")
+	protected List<IndexRange> resolveDataColumnRanges(String dataColumnExp) throws DataSetException
+	{
+		List<Range> ranges = getRangeExpResolver().resolve(dataColumnExp);
+
+		if (ranges == null || ranges.isEmpty())
+			return Collections.EMPTY_LIST;
+
+		List<IndexRange> indexRanges = new ArrayList<>(ranges.size());
+
+		for (Range range : ranges)
+		{
+			int from = 0;
+			int to = -1;
+
+			String fromStr = range.trimFrom();
+			String toStr = range.trimTo();
+
+			if (!StringUtil.isEmpty(fromStr))
+				from = CellReference.convertColStringToIndex(fromStr);
+
+			if (!StringUtil.isEmpty(toStr))
+				to = CellReference.convertColStringToIndex(toStr);
+
+			indexRanges.add(new IndexRange(from, to));
+		}
+
+		return indexRanges;
+	}
+
+	/**
+	 * 给定Excel文件是否是老版本的{@code .xls}文件。
+	 * 
+	 * @param file
+	 * @return
+	 */
+	protected boolean isXls(File file)
+	{
+		if (this.forceXls)
+			return true;
+
+		return FileUtil.isExtension(file, EXTENSION_XLS);
+	}
+
+	protected RangeExpResolver getRangeExpResolver()
+	{
+		return RANGE_EXP_RESOLVER;
+	}
+
+	/**
+	 * 获取Excel文件。
+	 * <p>
+	 * 实现方法应该返回实例级不变的文件。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws Throwable
+	 */
+	protected abstract File getExcelFile(DataSetQuery query) throws Throwable;
+
+	protected static class ExcelPropertyInfo implements Serializable
+	{
+		private static final long serialVersionUID = 1L;
+
+		/** 属性名 */
+		private final String name;
+
+		/**
+		 * 单元格序号,介于{@linkplain Row#getFirstCellNum()}和{@linkplain Row#getLastCellNum()}之间
+		 */
+		private final short cellIdx;
+
+		public ExcelPropertyInfo(String name, short cellIdx)
+		{
+			super();
+			this.name = name;
+			this.cellIdx = cellIdx;
+		}
+
+		public String getName()
+		{
+			return name;
+		}
+
+		public short getCellIdx()
+		{
+			return cellIdx;
+		}
+	}
+}

+ 387 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractJsonDataSet.java

@@ -0,0 +1,387 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.DataSetResult;
+import org.datagear.analysis.ResolvableDataSet;
+import org.datagear.analysis.ResolvedDataSetResult;
+import org.datagear.analysis.support.fmk.JsonOutputFormat;
+import org.datagear.util.IOUtil;
+import org.datagear.util.StringUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.ValueNode;
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
+import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
+
+/**
+ * 抽象JSON数据集。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractJsonDataSet extends AbstractResolvableDataSet implements ResolvableDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final DataSetFmkTemplateResolver JSON_TEMPLATE_RESOLVER = new DataSetFmkTemplateResolver(
+			JsonOutputFormat.INSTANCE);
+
+	/** 使用Jackson的{@code JSONPath}配置 */
+	protected static final Configuration JACKSON_JSON_PATH_CONFIGURATION = Configuration.builder()
+			.jsonProvider(new JacksonJsonProvider()).mappingProvider(new JacksonMappingProvider()).build();
+
+	/** 数据JSON路径 */
+	private String dataJsonPath = "";
+
+	public AbstractJsonDataSet()
+	{
+		super();
+	}
+
+	public AbstractJsonDataSet(String id, String name)
+	{
+		super(id, name);
+	}
+
+	public AbstractJsonDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	public String getDataJsonPath()
+	{
+		return dataJsonPath;
+	}
+
+	/**
+	 * 设置数据JSON路径。
+	 * <p>
+	 * 当希望返回的是原始JSON数据的指定JSON路径值时,可以设置此项。
+	 * </p>
+	 * <p>
+	 * 例如:"stores[0].books"、"[1].stores"、"$['store']['book'][0]"、
+	 * "$.store.book[*].author"、"$..book[2]",具体参考{@code JSONPath}相关文档。
+	 * </p>
+	 * <p>
+	 * 默认无数据路径,将直接返回原始JSON数据。
+	 * </p>
+	 * 
+	 * @param dataJsonPath
+	 */
+	public void setDataJsonPath(String dataJsonPath)
+	{
+		this.dataJsonPath = dataJsonPath;
+	}
+
+	/**
+	 * 解析结果。
+	 * <p>
+	 * 如果{@linkplain #getJsonReader(Map)}返回的{@linkplain TemplateResolvedSource#hasResolvedTemplate()},
+	 * 此方法将返回{@linkplain TemplateResolvedDataSetResult}。
+	 * </p>
+	 */
+	@Override
+	protected ResolvedDataSetResult resolveResult(DataSetQuery query, List<DataSetProperty> properties,
+			boolean resolveProperties) throws DataSetException
+	{
+		TemplateResolvedSource<Reader> reader = null;
+		try
+		{
+			reader = getJsonReader(query);
+
+			ResolvedDataSetResult result = resolveResult(query, reader.getSource(), properties, resolveProperties);
+
+			if (reader.hasResolvedTemplate())
+				result = new TemplateResolvedDataSetResult(result.getResult(), result.getProperties(),
+						reader.getResolvedTemplate());
+
+			return result;
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t, reader.getResolvedTemplate());
+		}
+		finally
+		{
+			if (reader != null)
+				IOUtil.close(reader.getSource());
+		}
+	}
+
+	/**
+	 * 获取JSON输入流。
+	 * <p>
+	 * 实现方法应该返回实例级不变的输入流。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws Throwable
+	 */
+	protected abstract TemplateResolvedSource<Reader> getJsonReader(DataSetQuery query) throws Throwable;
+
+	/**
+	 * 解析结果。
+	 * 
+	 * @param query
+	 * @param jsonReader
+	 *            JSON输入流
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 * @return
+	 * @throws Throwable
+	 */
+	protected ResolvedDataSetResult resolveResult(DataSetQuery query,
+			Reader jsonReader, List<DataSetProperty> properties, boolean resolveProperties) throws Throwable
+	{
+		JsonNode jsonNode = getObjectMapperNonStardand().readTree(jsonReader);
+
+		if (!isLegalResultDataJsonNode(jsonNode))
+			throw new UnsupportedJsonResultDataException("Result data must be JSON object or array");
+
+		Object rawData = resolveRawData(query, jsonNode, getDataJsonPath());
+
+		if (resolveProperties)
+		{
+			List<DataSetProperty> resolvedProperties = resolveProperties(rawData);
+			mergeDataSetProperties(resolvedProperties, properties);
+			properties = resolvedProperties;
+		}
+
+		return resolveResult(rawData, properties, query.getResultDataFormat());
+	}
+
+	/**
+	 * 解析原始数据数据。
+	 * 
+	 * @param query
+	 * @param jsonNode      允许为{@code null}
+	 * @param dataJsonPath  允许为{@code null}
+	 * @return
+	 * @throws ReadJsonDataPathException
+	 * @throws Throwable
+	 */
+	protected Object resolveRawData(DataSetQuery query, JsonNode jsonNode, String dataJsonPath)
+			throws ReadJsonDataPathException, Throwable
+	{
+		if (jsonNode == null)
+			return null;
+
+		Object data = getObjectMapperNonStardand().treeToValue(jsonNode, Object.class);
+
+		if (data != null && !StringUtil.isEmpty(dataJsonPath))
+		{
+			String stdDataJsonPath = dataJsonPath.trim();
+
+			if (!StringUtil.isEmpty(stdDataJsonPath))
+			{
+				// 转换"stores[0].books"、"[1].stores"简化模式为规范的JSONPath
+				if (!stdDataJsonPath.startsWith("$"))
+				{
+					if (stdDataJsonPath.startsWith("["))
+						stdDataJsonPath = "$" + stdDataJsonPath;
+					else
+						stdDataJsonPath = "$." + stdDataJsonPath;
+				}
+
+				try
+				{
+					data = JsonPath.compile(stdDataJsonPath).read(data, JACKSON_JSON_PATH_CONFIGURATION);
+				}
+				catch(Throwable t)
+				{
+					throw new ReadJsonDataPathException(dataJsonPath, t);
+				}
+			}
+		}
+
+		if (data != null && hasResultFetchSize(query))
+		{
+			if (data instanceof Collection<?>)
+			{
+				Collection<?> collection = (List<?>) data;
+				List<Object> dataList = new ArrayList<>();
+
+				for (Object ele : collection)
+				{
+					if (isReachResultFetchSize(query, dataList.size()))
+						break;
+
+					dataList.add(ele);
+				}
+
+				data = dataList;
+			}
+			else if (data instanceof Object[])
+			{
+				Object[] array = (Object[]) data;
+				Object[] dataArray = new Object[evalResultFetchSize(query, array.length)];
+
+				for (int i = 0; i < dataArray.length; i++)
+				{
+					dataArray[i] = array[i];
+				}
+
+				data = dataArray;
+			}
+		}
+
+		return data;
+	}
+
+	/**
+	 * 是否是合法的数据集结果数据{@linkplain JsonNode}。
+	 * <p>
+	 * 参考{@linkplain DataSetResult#getData()}说明。
+	 * </p>
+	 * 
+	 * @param jsonNode
+	 *            允许为{@code null}
+	 * @return
+	 */
+	protected boolean isLegalResultDataJsonNode(JsonNode jsonNode) throws Throwable
+	{
+		if (jsonNode == null || jsonNode.isNull())
+			return true;
+
+		if (jsonNode instanceof ValueNode)
+			return false;
+
+		if (jsonNode instanceof ArrayNode)
+		{
+			ArrayNode arrayNode = (ArrayNode) jsonNode;
+
+			for (int i = 0; i < arrayNode.size(); i++)
+			{
+				JsonNode eleNode = arrayNode.get(i);
+
+				if (eleNode == null || eleNode.isNull())
+					continue;
+
+				if (!(eleNode instanceof ObjectNode))
+					return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * 解析{@linkplain DataSetProperty}。
+	 * 
+	 * @param resultData 允许为{@code null},JSON对象、JSON对象数组、JSON对象列表
+	 * @return
+	 * @throws Throwable
+	 */
+	@SuppressWarnings("unchecked")
+	protected List<DataSetProperty> resolveProperties(Object resultData) throws Throwable
+	{
+		if (resultData == null)
+		{
+			return Collections.EMPTY_LIST;
+		}
+		else if (resultData instanceof Map<?, ?>)
+		{
+			return resolveJsonObjProperties((Map<String, ?>) resultData);
+		}
+		else if (resultData instanceof List<?>)
+		{
+			List<?> list = (List<?>) resultData;
+
+			if (list.size() == 0)
+				return Collections.EMPTY_LIST;
+			else
+				return resolveJsonObjProperties((Map<String, ?>) list.get(0));
+		}
+		else if (resultData instanceof Object[])
+		{
+			Object[] array = (Object[]) resultData;
+
+			if (array.length == 0)
+				return Collections.EMPTY_LIST;
+			else
+				return resolveJsonObjProperties((Map<String, ?>) array[0]);
+		}
+		else
+			throw new UnsupportedJsonResultDataException("Result data must be object or object array/list");
+	}
+
+	/**
+	 * 解析{@linkplain DataSetProperty}。
+	 * 
+	 * @param jsonObj
+	 * @return
+	 * @throws Throwable
+	 */
+	protected List<DataSetProperty> resolveJsonObjProperties(Map<String, ?> jsonObj) throws Throwable
+	{
+		List<DataSetProperty> properties = new ArrayList<>();
+
+		if (jsonObj == null)
+		{
+
+		}
+		else
+		{
+			for (Map.Entry<String, ?> entry : jsonObj.entrySet())
+			{
+				Object value = entry.getValue();
+				String type = DataSetProperty.DataType.resolveDataType(value);
+
+				DataSetProperty property = new DataSetProperty(entry.getKey(), type);
+
+				// JSON数值只有NUMBER类型
+				if (DataSetProperty.DataType.INTEGER.equals(property.getType())
+						|| DataSetProperty.DataType.DECIMAL.equals(property.getType()))
+					property.setType(DataSetProperty.DataType.NUMBER);
+
+				properties.add(property);
+			}
+		}
+
+		return properties;
+	}
+
+	protected ObjectMapper getObjectMapperNonStardand()
+	{
+		return JsonSupport.getObjectMapperNonStardand();
+	}
+
+	/**
+	 * 将指定JSON文本作为模板解析。
+	 * 
+	 * @param json
+	 * @param query
+	 * @return
+	 */
+	protected String resolveJsonAsTemplate(String json, DataSetQuery query)
+	{
+		return resolveTextAsTemplate(JSON_TEMPLATE_RESOLVER, json, query);
+	}
+}

+ 77 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractJsonFileDataSet.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.Reader;
+import java.util.List;
+
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.IOUtil;
+
+/**
+ * 抽象JSON文件数据集。
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractJsonFileDataSet extends AbstractJsonDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 文件编码 */
+	private String encoding = IOUtil.CHARSET_UTF_8;
+
+	public AbstractJsonFileDataSet()
+	{
+		super();
+	}
+
+	public AbstractJsonFileDataSet(String id, String name)
+	{
+		super(id, name);
+	}
+
+	public AbstractJsonFileDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	public String getEncoding()
+	{
+		return encoding;
+	}
+
+	public void setEncoding(String encoding)
+	{
+		this.encoding = encoding;
+	}
+
+	@Override
+	protected TemplateResolvedSource<Reader> getJsonReader(DataSetQuery query) throws Throwable
+	{
+		File file = getJsonFile(query);
+		return new TemplateResolvedSource<>(IOUtil.getReader(file, this.encoding));
+	}
+
+	/**
+	 * 获取JSON文件。
+	 * <p>
+	 * 实现方法应该返回实例级不变的文件。
+	 * </p>
+	 * 
+	 * @param query
+	 * @return
+	 * @throws Throwable
+	 */
+	protected abstract File getJsonFile(DataSetQuery query) throws Throwable;
+}

+ 145 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractResolvableDataSet.java

@@ -0,0 +1,145 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.DataSetResult;
+import org.datagear.analysis.ResolvableDataSet;
+import org.datagear.analysis.ResolvedDataSetResult;
+
+/**
+ * 抽象{@linkplain ResolvableDataSet}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractResolvableDataSet extends AbstractDataSet implements ResolvableDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	public AbstractResolvableDataSet()
+	{
+		super();
+	}
+
+	@SuppressWarnings("unchecked")
+	public AbstractResolvableDataSet(String id, String name)
+	{
+		super(id, name, Collections.EMPTY_LIST);
+	}
+
+	public AbstractResolvableDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	@Override
+	public DataSetResult getResult(DataSetQuery query) throws DataSetException
+	{
+		checkRequiredParamValues(query);
+
+		List<DataSetProperty> properties = getProperties();
+		ResolvedDataSetResult result = resolveResult(query, properties, false);
+
+		return result.getResult();
+	}
+
+	@Override
+	public ResolvedDataSetResult resolve(DataSetQuery query) throws DataSetException
+	{
+		checkRequiredParamValues(query);
+
+		List<DataSetProperty> properties = getProperties();
+
+		return resolveResult(query, properties, true);
+	}
+
+	/**
+	 * 解析结果。
+	 * 
+	 * @param query
+	 * @param properties
+	 *            允许为{@code null}
+	 * @param resolveProperties
+	 *            是否从数据中解析{@linkplain DataSetProperty},如果为{@code true},将解析且合并{@code properties}参数,
+	 *            并设置为{@linkplain ResolvedDataSetResult#setProperties(List)};如果为{@code false},
+	 *            将把{@code properties}参数直接设置为{@linkplain ResolvedDataSetResult#setProperties(List)}
+	 * @return
+	 * @throws DataSetException
+	 */
+	protected abstract ResolvedDataSetResult resolveResult(DataSetQuery query,
+			List<DataSetProperty> properties, boolean resolveProperties) throws DataSetException;
+
+	/**
+	 * 合并{@linkplain DataSetProperty}。
+	 * <p>
+	 * 将{@code merged}待合并项里的{@linkplain DataSetProperty#getType()}、{@linkplain DataSetProperty#getLabel()}、
+	 * {@linkplain DataSetProperty#getDefaultValue()}合并至{@code dataSetProperties}里的同名项,
+	 * 多余项则直接添加至{@code dataSetProperties},另外,也会根据{@code merged}里的排序对{@code dataSetProperties}重排。
+	 * </p>
+	 * 
+	 * @param dataSetProperties
+	 *            必须是可编辑的列表
+	 * @param merged
+	 *            待合并项,允许为{@code null}
+	 * @return
+	 */
+	protected void mergeDataSetProperties(List<? extends DataSetProperty> dataSetProperties,
+			List<? extends DataSetProperty> merged)
+	{
+		if (merged == null)
+			merged = Collections.emptyList();
+
+		@SuppressWarnings("unchecked")
+		List<DataSetProperty> dps = (List<DataSetProperty>) dataSetProperties;
+
+		for (DataSetProperty dp : dps)
+		{
+			DataSetProperty mp = getDataNameTypeByName(merged, dp.getName());
+			
+			if(mp != null)
+			{
+				dp.setType(mp.getType());
+				dp.setLabel(mp.getLabel());
+				dp.setDefaultValue(mp.getDefaultValue());
+			}
+		}
+
+		for (DataSetProperty mp : merged)
+		{
+			if (getDataNameTypeByName(dps, mp.getName()) == null)
+				dps.add(mp);
+		}
+
+		final List<? extends DataSetProperty> mergedFinal = merged;
+
+		dps.sort(new Comparator<DataSetProperty>()
+		{
+			@Override
+			public int compare(DataSetProperty o1, DataSetProperty o2)
+			{
+				// 优先按照merged列表中的顺序重排
+				int o1Idx = getDataNameTypeIndexByName(mergedFinal, o1.getName());
+				int o2Idx = getDataNameTypeIndexByName(mergedFinal, o2.getName());
+
+				if (o1Idx < 0)
+					o1Idx = getDataNameTypeIndexByName(dps, o1.getName());
+				if (o2Idx < 0)
+					o2Idx = getDataNameTypeIndexByName(dps, o2.getName());
+
+				return Integer.valueOf(o1Idx).compareTo(o2Idx);
+			}
+		});
+	}
+}

+ 89 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractTemplateDashboardWidgetResManager.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+
+import org.datagear.analysis.TemplateDashboardWidget;
+import org.datagear.analysis.TemplateDashboardWidgetResManager;
+import org.datagear.util.IOUtil;
+import org.datagear.util.StringUtil;
+
+/**
+ * 抽象{@linkplain TemplateDashboardWidgetResManager}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class AbstractTemplateDashboardWidgetResManager implements TemplateDashboardWidgetResManager
+{
+	public AbstractTemplateDashboardWidgetResManager()
+	{
+		super();
+	}
+
+	@Override
+	public String getDefaultEncoding()
+	{
+		return Charset.defaultCharset().name();
+	}
+
+	@Override
+	public Reader getReader(TemplateDashboardWidget widget, String name) throws IOException
+	{
+		String encoding = getResourceEncodingWithDefault(widget);
+		return getReader(widget.getId(), name, encoding);
+	}
+
+	@Override
+	public Reader getReader(String id, String name, String encoding) throws IOException
+	{
+		encoding = getResourceEncodingWithDefault(encoding);
+		InputStream in = getInputStream(id, name);
+		return IOUtil.getReader(in, encoding);
+	}
+
+	@Override
+	public Writer getWriter(TemplateDashboardWidget widget, String name) throws IOException
+	{
+		String encoding = getResourceEncodingWithDefault(widget);
+		return getWriter(widget.getId(), name, encoding);
+	}
+
+	@Override
+	public Writer getWriter(String id, String name, String encoding) throws IOException
+	{
+		encoding = getResourceEncodingWithDefault(encoding);
+
+		OutputStream out = getOutputStream(id, name);
+		return IOUtil.getWriter(out, encoding);
+	}
+
+	protected String getResourceEncodingWithDefault(TemplateDashboardWidget widget)
+	{
+		String encoding = widget.getTemplateEncoding();
+
+		if (StringUtil.isEmpty(encoding))
+			encoding = getDefaultEncoding();
+
+		return encoding;
+	}
+
+	protected String getResourceEncodingWithDefault(String encoding)
+	{
+		if (StringUtil.isEmpty(encoding))
+			encoding = getDefaultEncoding();
+
+		return encoding;
+	}
+}

+ 179 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/BytesIcon.java

@@ -0,0 +1,179 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.datagear.analysis.Icon;
+import org.datagear.util.FileUtil;
+import org.datagear.util.IOUtil;
+
+/**
+ * 字节数组{@linkplain Icon}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class BytesIcon implements Icon
+{
+	private static final long serialVersionUID = 1L;
+
+	private String type;
+
+	private byte[] bytes;
+
+	private long lastModified;
+
+	public BytesIcon()
+	{
+	}
+
+	public BytesIcon(String type, byte[] bytes, long lastModified)
+	{
+		super();
+		this.type = type;
+		this.bytes = bytes;
+		this.lastModified = lastModified;
+	}
+
+	@Override
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setType(String type)
+	{
+		this.type = type;
+	}
+
+	public byte[] getBytes()
+	{
+		return bytes;
+	}
+
+	public void setBytes(byte[] bytes)
+	{
+		this.bytes = bytes;
+	}
+
+	@Override
+	public long getLastModified()
+	{
+		return lastModified;
+	}
+
+	public void setLastModified(long lastModified)
+	{
+		this.lastModified = lastModified;
+	}
+
+	@Override
+	public InputStream getInputStream() throws IOException
+	{
+		return new ByteArrayInputStream(this.bytes);
+	}
+
+	/**
+	 * 构建{@linkplain BytesIcon}。
+	 * 
+	 * @param bytes
+	 * @param lastModified
+	 * @return
+	 */
+	public static BytesIcon valueOf(String type, byte[] bytes, long lastModified)
+	{
+		return new BytesIcon(type, bytes, lastModified);
+	}
+
+	/**
+	 * 构建{@linkplain BytesIcon}。
+	 * 
+	 * @param file
+	 * @return
+	 * @throws IOException
+	 */
+	public static BytesIcon valueOf(File file) throws IOException
+	{
+		return valueOf(FileUtil.getExtension(file), file);
+	}
+
+	/**
+	 * 构建{@linkplain BytesIcon}。
+	 * 
+	 * @param type
+	 * @param file
+	 * @return
+	 * @throws IOException
+	 */
+	public static BytesIcon valueOf(String type, File file) throws IOException
+	{
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		try
+		{
+			IOUtil.write(file, out);
+		}
+		finally
+		{
+			IOUtil.close(out);
+		}
+
+		return new BytesIcon(type, out.toByteArray(), file.lastModified());
+	}
+
+	/**
+	 * 构建{@linkplain BytesIcon}。
+	 * <p>
+	 * 它不会关闭{@code in}输入流。
+	 * </p>
+	 * 
+	 * @param type
+	 * @param in
+	 * @param lastModified
+	 * @return
+	 * @throws IOException
+	 */
+	public static BytesIcon valueOf(String type, InputStream in, long lastModified) throws IOException
+	{
+		return valueOf(type, in, lastModified, false);
+	}
+
+	/**
+	 * 构建{@linkplain BytesIcon}。
+	 * 
+	 * @param type
+	 * @param in
+	 * @param lastModified
+	 * @param closeIn
+	 * @return
+	 * @throws IOException
+	 */
+	public static BytesIcon valueOf(String type, InputStream in, long lastModified, boolean closeIn) throws IOException
+	{
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		try
+		{
+			IOUtil.write(in, out);
+		}
+		finally
+		{
+			if (closeIn)
+				IOUtil.close(in);
+
+			IOUtil.close(out);
+		}
+
+		return new BytesIcon(type, out.toByteArray(), lastModified);
+	}
+}

+ 146 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/CategorizationResolver.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.datagear.analysis.Category;
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.util.StringUtil;
+
+/**
+ * 将{@linkplain ChartPlugin}按照{@linkplain Category}分组处理器。
+ * <p>
+ * 此类是线程安全的。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class CategorizationResolver
+{
+	public CategorizationResolver()
+	{
+		super();
+	}
+
+	/**
+	 * 分类。
+	 * 
+	 * @param chartPlugins
+	 * @return 最后一个元素包含所有未分组的{@linkplain ChartPlugin}
+	 */
+	public List<Categorization> resolve(List<? extends ChartPlugin> chartPlugins)
+	{
+		List<Categorization> categorizations = new ArrayList<>();
+		List<ChartPlugin> uncategorizeds = new ArrayList<>();
+
+		for (ChartPlugin chartPlugin : chartPlugins)
+		{
+			Category category = chartPlugin.getCategory();
+
+			if (category == null || StringUtil.isEmpty(category.getName()))
+				uncategorizeds.add(chartPlugin);
+			else
+			{
+				Categorization categorization = null;
+
+				for (Categorization myCategorization : categorizations)
+				{
+					Category myCategory = myCategorization.getCategory();
+
+					if (category.getName().equals(myCategory.getName()))
+					{
+						categorization = myCategorization;
+
+						// 使用信息最全的那个
+						if (category.hasNameLabel())
+							categorization.setCategory(category);
+					}
+				}
+
+				if (categorization == null)
+				{
+					categorization = new Categorization(category);
+					categorizations.add(categorization);
+				}
+
+				categorization.addChartPlugin(chartPlugin);
+			}
+		}
+
+		Collections.sort(categorizations, CATEGORIZATION_COMPARATOR);
+
+		if (!uncategorizeds.isEmpty())
+		{
+			Categorization uncategorized = new Categorization(new Category(""));
+			uncategorized.setChartPlugins(uncategorizeds);
+			categorizations.add(uncategorized);
+		}
+
+		return categorizations;
+	}
+
+	protected static final Comparator<Categorization> CATEGORIZATION_COMPARATOR = new Comparator<Categorization>()
+	{
+		@Override
+		public int compare(Categorization o1, Categorization o2)
+		{
+			return o1.getCategory().getOrder() - o2.getCategory().getOrder();
+		}
+	};
+
+	/**
+	 * 分类结果。
+	 */
+	public static class Categorization
+	{
+		private Category category;
+
+		private List<ChartPlugin> chartPlugins = new ArrayList<>(5);
+
+		public Categorization()
+		{
+			super();
+		}
+
+		public Categorization(Category category)
+		{
+			super();
+			this.category = category;
+		}
+
+		public Category getCategory()
+		{
+			return category;
+		}
+
+		public void setCategory(Category category)
+		{
+			this.category = category;
+		}
+
+		public List<ChartPlugin> getChartPlugins()
+		{
+			return chartPlugins;
+		}
+
+		public void setChartPlugins(List<ChartPlugin> chartPlugins)
+		{
+			this.chartPlugins = chartPlugins;
+		}
+
+		public void addChartPlugin(ChartPlugin chartPlugin)
+		{
+			this.chartPlugins.add(chartPlugin);
+		}
+	}
+}

+ 38 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ChartParamValueConverter.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import org.datagear.analysis.ChartParam;
+import org.datagear.analysis.ChartParam.DataType;
+
+/**
+ * {@linkplain ChartParam}值转换器。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartParamValueConverter extends DataValueConverter
+{
+	public ChartParamValueConverter()
+	{
+		super();
+	}
+
+	@Override
+	protected Object convertValue(Object value, String type) throws DataValueConvertionException
+	{
+		if (DataType.STRING.equals(type))
+			return convertToString(value, DataType.STRING);
+		else if (DataType.NUMBER.equals(type))
+			return convertToNumber(type, DataType.NUMBER);
+		else if (DataType.BOOLEAN.equals(type))
+			return convertToBoolean(value, DataType.BOOLEAN);
+		else
+			return convertExt(value, type);
+	}
+}

+ 90 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ChartResultErrorMessage.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Serializable;
+
+import org.datagear.analysis.ChartResultError;
+
+/**
+ * 图表结果错误消息。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartResultErrorMessage implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 错误类型 */
+	private String type = "";
+
+	/** 错误消息 */
+	private String message = "";
+
+	public ChartResultErrorMessage()
+	{
+		super();
+	}
+
+	public ChartResultErrorMessage(String type, String message)
+	{
+		super();
+		this.type = type;
+		this.message = message;
+	}
+
+	public ChartResultErrorMessage(ChartResultError error)
+	{
+		this(error, false);
+	}
+
+	public ChartResultErrorMessage(ChartResultError error, boolean rootCauseMessage)
+	{
+		Throwable throwable = error.getThrowable();
+
+		if (throwable != null)
+		{
+			this.type = throwable.getClass().getSimpleName();
+
+			if (rootCauseMessage)
+			{
+				Throwable cause = throwable;
+
+				while (cause.getCause() != null)
+					cause = cause.getCause();
+
+				this.message = cause.getMessage();
+			}
+			else
+			{
+				this.message = throwable.getMessage();
+			}
+		}
+	}
+
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setType(String type)
+	{
+		this.type = type;
+	}
+
+	public String getMessage()
+	{
+		return message;
+	}
+
+	public void setMessage(String message)
+	{
+		this.message = message;
+	}
+}

+ 140 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ChartWidget.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.datagear.analysis.Chart;
+import org.datagear.analysis.ChartDataSet;
+import org.datagear.analysis.ChartDefinition;
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.analysis.ChartPluginManager;
+import org.datagear.analysis.RenderContext;
+import org.datagear.analysis.RenderException;
+import org.datagear.util.IDUtil;
+
+/**
+ * 图表部件。
+ * <p>
+ * 它可在{@linkplain RenderContext}中渲染自己所描述的{@linkplain Chart}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ChartWidget extends ChartDefinition implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 图表部件渲染时的部件信息属性名 */
+	public static final String ATTR_CHART_WIDGET = "chartWidget";
+
+	private ChartPlugin plugin;
+
+	public ChartWidget()
+	{
+		super();
+	}
+
+	public ChartWidget(String id, String name, ChartDataSet[] chartDataSets, ChartPlugin plugin)
+	{
+		super(id, name, chartDataSets);
+		this.plugin = plugin;
+	}
+
+	public ChartWidget(ChartDefinition chartDefinition, ChartPlugin plugin)
+	{
+		super(chartDefinition);
+		this.plugin = plugin;
+	}
+
+	public ChartPlugin getPlugin()
+	{
+		return plugin;
+	}
+
+	public void setPlugin(ChartPlugin plugin)
+	{
+		this.plugin = plugin;
+	}
+
+	/**
+	 * 从{@linkplain ChartPluginManager}查找并设置{@linkplain #setPlugin(ChartPlugin)}。
+	 * 
+	 * @param chartPluginManager
+	 * @param chartPluginId
+	 * @return
+	 */
+	public void setPlugin(ChartPluginManager chartPluginManager, String chartPluginId)
+	{
+		ChartPlugin chartPlugin = chartPluginManager.get(chartPluginId);
+		setPlugin(chartPlugin);
+	}
+
+	/**
+	 * 渲染{@linkplain Chart}。
+	 * 
+	 * @param renderContext
+	 * @return
+	 * @throws RenderException
+	 */
+	public Chart render(RenderContext renderContext) throws RenderException
+	{
+		return this.plugin.renderChart(renderContext, buildChartDefinition(renderContext));
+	}
+
+	protected ChartDefinition buildChartDefinition(RenderContext renderContext) throws RenderException
+	{
+		ChartDefinition chartDefinition = new ChartDefinition(this);
+		chartDefinition.setId(generateChartId(renderContext));
+
+		// 添加图表对应的部件信息
+		Map<String, Object> attributesNew = new HashMap<>();
+		Map<String, Object> attributesOld = chartDefinition.getAttributes();
+		if (attributesOld != null)
+			attributesNew.putAll(attributesOld);
+		Map<String, Object> chartWidgetInfo = new HashMap<>();
+		chartWidgetInfo.put(ChartWidget.PROPERTY_ID, this.getId());
+		attributesNew.put(ATTR_CHART_WIDGET, chartWidgetInfo);
+
+		chartDefinition.setAttributes(attributesNew);
+
+		return chartDefinition;
+	}
+
+	/**
+	 * 生成图表ID。
+	 * 
+	 * @param renderContext
+	 * @return
+	 * @throws RenderException
+	 */
+	protected String generateChartId(RenderContext renderContext) throws RenderException
+	{
+		return IDUtil.uuid();
+	}
+
+	/**
+	 * 获取由{@linkplain ChartWidget#render(RenderContext)}渲染的{@linkplain Chart}所关联的{@linkplain ChartWidget#getId()}。
+	 * 
+	 * @param chart
+	 * @return 可能返回{@code null}
+	 */
+	public static String getChartWidget(Chart chart)
+	{
+		if (chart == null)
+			return null;
+
+		@SuppressWarnings("unchecked")
+		Map<String, Object> chartWidgetInfo = (Map<String, Object>) chart.getAttribute(ATTR_CHART_WIDGET);
+
+		return (chartWidgetInfo == null ? null : (String) chartWidgetInfo.get(ChartWidget.PROPERTY_ID));
+	}
+}

+ 26 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ChartWidgetSource.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+/**
+ * {@linkplain ChartWidget}源。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public interface ChartWidgetSource
+{
+	/**
+	 * 获取指定ID的{@linkplain ChartWidget},没有则返回{@code null}。
+	 * 
+	 * @param id
+	 * @return
+	 * @throws Throwable
+	 */
+	ChartWidget getChartWidget(String id) throws Throwable;
+}

+ 120 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ConcurrentChartPluginManager.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.List;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
+
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.analysis.ChartPluginManager;
+
+/**
+ * 并发{@linkplain ChartPluginManager}。
+ * <p>
+ * 此类是线程安全的。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ConcurrentChartPluginManager extends AbstractChartPluginManager
+{
+	protected ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+	public ConcurrentChartPluginManager()
+	{
+		super();
+	}
+
+	@Override
+	public void register(ChartPlugin chartPlugin)
+	{
+		WriteLock writeLock = this.lock.writeLock();
+
+		try
+		{
+			writeLock.lock();
+
+			registerChartPlugin(chartPlugin);
+		}
+		finally
+		{
+			writeLock.unlock();
+		}
+	}
+
+	@Override
+	public ChartPlugin[] remove(String... ids)
+	{
+		WriteLock writeLock = this.lock.writeLock();
+
+		try
+		{
+			writeLock.lock();
+
+			return removeChartPlugins(ids);
+		}
+		finally
+		{
+			writeLock.unlock();
+		}
+	}
+
+	@Override
+	public ChartPlugin get(String id)
+	{
+		ReadLock readLock = this.lock.readLock();
+
+		try
+		{
+			readLock.lock();
+
+			return getChartPlugin(id);
+		}
+		finally
+		{
+			readLock.unlock();
+		}
+	}
+
+	@Override
+	public <T extends ChartPlugin> List<T> getAll(Class<? super T> chartPluginType)
+	{
+		ReadLock readLock = this.lock.readLock();
+
+		try
+		{
+			readLock.lock();
+
+			return findChartPlugins(chartPluginType);
+		}
+		finally
+		{
+			readLock.unlock();
+		}
+	}
+
+	@Override
+	public List<ChartPlugin> getAll()
+	{
+		ReadLock readLock = this.lock.readLock();
+
+		try
+		{
+			readLock.lock();
+
+			return getAllChartPlugins();
+		}
+		finally
+		{
+			readLock.unlock();
+		}
+	}
+}

+ 83 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/CsvDirectoryFileDataSet.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.util.List;
+
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.FileUtil;
+
+/**
+ * 目录内CSV文件{@linkplain DataSet}。
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class CsvDirectoryFileDataSet extends AbstractCsvFileDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** CSV文件所在的目录 */
+	private File directory;
+
+	/** CSV文件名 */
+	private String fileName;
+
+	public CsvDirectoryFileDataSet()
+	{
+		super();
+	}
+
+	public CsvDirectoryFileDataSet(String id, String name, File directory, String fileName)
+	{
+		super(id, name);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	public CsvDirectoryFileDataSet(String id, String name, List<DataSetProperty> properties, File directory,
+			String fileName)
+	{
+		super(id, name, properties);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	public File getDirectory()
+	{
+		return directory;
+	}
+
+	public void setDirectory(File directory)
+	{
+		this.directory = directory;
+	}
+
+	public String getFileName()
+	{
+		return fileName;
+	}
+
+	public void setFileName(String fileName)
+	{
+		this.fileName = fileName;
+	}
+
+	@Override
+	protected File getCsvFile(DataSetQuery query) throws Throwable
+	{
+		File jsonFile = FileUtil.getFile(this.directory, this.fileName);
+		return jsonFile;
+	}
+}

+ 85 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/CsvValueDataSet.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Reader;
+import java.util.List;
+
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.IOUtil;
+
+/**
+ * CSV值数据集。
+ * <p>
+ * 此类的{@linkplain #getValue()}支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class CsvValueDataSet extends AbstractCsvDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** CSV字符串 */
+	private String value;
+
+	public CsvValueDataSet()
+	{
+		super();
+	}
+
+	public CsvValueDataSet(String id, String name, String value)
+	{
+		super(id, name);
+		this.value = value;
+	}
+
+	public CsvValueDataSet(String id, String name, List<DataSetProperty> properties, String value)
+	{
+		super(id, name, properties);
+		this.value = value;
+	}
+
+	public String getValue()
+	{
+		return value;
+	}
+
+	/**
+	 * 设置CSV字符串值,格式为:
+	 * 
+	 * <pre>
+	 * name, value
+	 * aaa, 1
+	 * bbb, 2
+	 * </pre>
+	 * 
+	 * @param value
+	 */
+	public void setValue(String value)
+	{
+		this.value = value;
+	}
+
+	@Override
+	public TemplateResolvedDataSetResult resolve(DataSetQuery query)
+			throws DataSetException
+	{
+		return (TemplateResolvedDataSetResult) super.resolve(query);
+	}
+
+	@Override
+	protected TemplateResolvedSource<Reader> getCsvReader(DataSetQuery query) throws Throwable
+	{
+		String csv = resolveCsvAsTemplate(this.value, query);
+		return new TemplateResolvedSource<>(IOUtil.getReader(csv), csv);
+	}
+}

+ 28 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataFormat.java

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Serializable;
+
+import org.datagear.util.DateNumberFormat;
+
+/**
+ * 数据格式。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataFormat extends DateNumberFormat implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	public DataFormat()
+	{
+		super();
+	}
+}

+ 283 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetFmkTemplateResolver.java

@@ -0,0 +1,283 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import freemarker.cache.TemplateLoader;
+import freemarker.core.OutputFormat;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * 专用于数据集模板且采用Freemarker作为模板语言的{@linkplain TemplateResolver}。
+ * <p>
+ * 此类的{@linkplain #setDataSetTemplateStandardConfig(Configuration)}定义了很多数据集模板规范,
+ * 这些规范不应被更改,因为会影响用户已定义数据集的模板。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetFmkTemplateResolver implements TemplateResolver
+{
+	/**
+	 * Freemarker数值输出格式:computer
+	 * <p>
+	 * 这个格式会按照计算机编程格式输出数值,即:区域无关、无分隔符、无指数格式,例如:12345678950401.12345678950401
+	 * </p>
+	 * <p>
+	 * 具体参考Freemarker文档Built-ins for numbers章节的<code>c</code>指令。
+	 * </p>
+	 */
+	public static final String FREEMARKER_NUMBER_FORMAT_COMPUTER = "computer";
+
+	private NameTemplateLoader nameTemplateLoader;
+
+	private Configuration configuration;
+
+	public DataSetFmkTemplateResolver()
+	{
+		this(null, 1000);
+	}
+
+	public DataSetFmkTemplateResolver(OutputFormat outputFormat)
+	{
+		this(outputFormat, 1000);
+	}
+
+	public DataSetFmkTemplateResolver(OutputFormat outputFormat, int cacheCapacity)
+	{
+		super();
+		this.nameTemplateLoader = new NameTemplateLoader(cacheCapacity);
+
+		Configuration configuration = new Configuration(Configuration.VERSION_2_3_30);
+		configuration.setCacheStorage(new freemarker.cache.MruCacheStorage(0, cacheCapacity));
+
+		if (outputFormat != null)
+		configuration.setOutputFormat(outputFormat);
+
+		setConfiguration(configuration);
+	}
+
+	public NameTemplateLoader getNameTemplateLoader()
+	{
+		return nameTemplateLoader;
+	}
+
+	public void setNameTemplateLoader(NameTemplateLoader nameTemplateLoader)
+	{
+		this.nameTemplateLoader = nameTemplateLoader;
+
+		if (this.configuration != null)
+			this.configuration.setTemplateLoader(this.nameTemplateLoader);
+	}
+
+	public Configuration getConfiguration()
+	{
+		return configuration;
+	}
+
+	public void setConfiguration(Configuration configuration)
+	{
+		this.configuration = configuration;
+
+		if (this.nameTemplateLoader != null)
+			this.configuration.setTemplateLoader(this.nameTemplateLoader);
+
+		setDataSetTemplateStandardConfig(this.configuration);
+	}
+
+	/**
+	 * 设置用于数据集模板的语法规范。
+	 * 
+	 * @param configuration
+	 */
+	protected void setDataSetTemplateStandardConfig(Configuration configuration)
+	{
+		// 插值语法规范设置为:"${...}"
+		configuration.setInterpolationSyntax(Configuration.DOLLAR_INTERPOLATION_SYNTAX);
+
+		// 标签语法规范设置为:<#if>...</#if>
+		configuration.setTagSyntax(Configuration.ANGLE_BRACKET_TAG_SYNTAX);
+
+		// 数值插值设置为标准格式
+		configuration.setNumberFormat(FREEMARKER_NUMBER_FORMAT_COMPUTER);
+
+		// 由于此类的模板策略是直接使用模板作为模板名,如果此方法设置为true,
+		// 下面的NameTemplateLoader.findTemplateSource(String)的参数SQL会被加上Locale后缀导致逻辑出错,
+		// 因此这里必须设置为false
+		configuration.setLocalizedLookup(false);
+	}
+
+	/**
+	 * 使用指定数据集参数值解析模板。
+	 * 
+	 * @param template
+	 * @param paramValues
+	 * @return
+	 * @throws TemplateResolverException
+	 */
+	public String resolve(String template, Map<String, ?> paramValues) throws TemplateResolverException
+	{
+		return resolve(template, new TemplateContext(paramValues));
+	}
+
+	@Override
+	public String resolve(String template, TemplateContext templateContext) throws TemplateResolverException
+	{
+		String re = null;
+
+		Map<String, ?> values = templateContext.getValues();
+
+		try
+		{
+			Template templateObj = this.configuration.getTemplate(template);
+			StringWriter out = new StringWriter();
+			templateObj.process(values, out);
+			re = out.toString();
+		}
+		catch (IOException e)
+		{
+			throw new TemplateResolverException(e);
+		}
+		catch (TemplateException e)
+		{
+			throw new TemplateResolverException(e);
+		}
+
+		return re;
+	}
+
+	/**
+	 * 直接使用名称作为模板的{@linkplain TemplateLoader}。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class NameTemplateLoader implements TemplateLoader
+	{
+		private Cache<String, NameTemplateSource> nameTemplateCache;
+
+		public NameTemplateLoader(int cacheCapacity)
+		{
+			this(cacheCapacity, 60 * 60 * 24);
+		}
+
+		public NameTemplateLoader(int cacheCapacity, int cacheExpireSeconds)
+		{
+			super();
+
+			this.nameTemplateCache = Caffeine.newBuilder().maximumSize(cacheCapacity)
+					.expireAfterAccess(cacheExpireSeconds, TimeUnit.SECONDS).build();
+		}
+
+		protected Cache<String, NameTemplateSource> getSqlDataSetTemplateCache()
+		{
+			return nameTemplateCache;
+		}
+
+		protected void setSqlDataSetTemplateCache(Cache<String, NameTemplateSource> sqlDataSetTemplateCache)
+		{
+			this.nameTemplateCache = sqlDataSetTemplateCache;
+		}
+
+		@Override
+		public void closeTemplateSource(Object templateSource) throws IOException
+		{
+		}
+
+		@Override
+		public Object findTemplateSource(String name) throws IOException
+		{
+			return this.nameTemplateCache.get(name, new Function<String, NameTemplateSource>()
+			{
+				@Override
+				public NameTemplateSource apply(String name)
+				{
+					return new NameTemplateSource(name, System.currentTimeMillis());
+				}
+			});
+		}
+
+		@Override
+		public long getLastModified(Object templateSource)
+		{
+			return ((NameTemplateSource) templateSource).getLastModified();
+		}
+
+		@Override
+		public Reader getReader(Object templateSource, String encoding) throws IOException
+		{
+			return new StringReader(((NameTemplateSource) templateSource).getName());
+		}
+
+		protected static class NameTemplateSource
+		{
+			private final String name;
+
+			private final long lastModified;
+
+			public NameTemplateSource(String name, long lastModified)
+			{
+				super();
+				this.name = name;
+				this.lastModified = lastModified;
+			}
+
+			public String getName()
+			{
+				return name;
+			}
+
+			public long getLastModified()
+			{
+				return lastModified;
+			}
+
+			@Override
+			public int hashCode()
+			{
+				final int prime = 31;
+				int result = 1;
+				result = prime * result + ((name == null) ? 0 : name.hashCode());
+				return result;
+			}
+
+			@Override
+			public boolean equals(Object obj)
+			{
+				if (this == obj)
+					return true;
+				if (obj == null)
+					return false;
+				if (getClass() != obj.getClass())
+					return false;
+				NameTemplateSource other = (NameTemplateSource) obj;
+				if (name == null)
+				{
+					if (other.name != null)
+						return false;
+				}
+				else if (!name.equals(other.name))
+					return false;
+				return true;
+			}
+		}
+	}
+}

+ 122 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetParamValueConverter.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.datagear.analysis.DataNameType;
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetParam;
+import org.datagear.analysis.DataSetParam.DataType;
+import org.datagear.analysis.DataSetQuery;
+
+/**
+ * {@linkplain DataSetParam}值转换器。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetParamValueConverter extends DataValueConverter
+{
+	public DataSetParamValueConverter()
+	{
+		super();
+	}
+	
+	/**
+	 * 将{@linkplain DataSetQuery#getParamValues()}转换为匹配{@linkplain DataSet#getParams()}类型的映射表,
+	 * 并返回一个新的{@linkplain DataSetQuery}。
+	 * <p>
+	 * 如果{@linkplain DataSetQuery#getParamValues()}中有未在{@linkplain DataSet#getParams()}中定义的项,
+	 * 那么它将原样写入返回的{@linkplain DataSetQuery}。
+	 * </p>
+	 * <p>
+	 * 因为对于支持<code>Freemarker</code>的{@linkplain DataSet}实现类(比如:{@linkplain SqlDataSet}),
+	 * 存在不定义{@linkplain DataSet#getParams()}而传递参数给内部<code>Freemarker</code>模板的应用场景,
+	 * 也会存在传递系统上下文变量的场景(比如传递系统当前用户)。
+	 * </p>
+	 * 
+	 * @param query
+	 * @param dataSet
+	 * @return 当{@code query}为{@code null}时 ,将返回{@code null}
+	 */
+	public DataSetQuery convert(DataSetQuery query, DataSet dataSet)
+	{
+		return convert(query, dataSet, false);
+	}
+	
+	/**
+	 * 将{@linkplain DataSetQuery#getParamValues()}转换为匹配{@linkplain DataSet#getParams()}类型的映射表,
+	 * 并返回一个新的{@linkplain DataSetQuery}。
+	 * <p>
+	 * 如果{@linkplain DataSetQuery#getParamValues()}中有未在{@linkplain DataSet#getParams()}中定义的项,
+	 * 那么它将原样写入返回的{@linkplain DataSetQuery}。
+	 * </p>
+	 * <p>
+	 * 因为对于支持<code>Freemarker</code>的{@linkplain DataSet}实现类(比如:{@linkplain SqlDataSet}),
+	 * 存在不定义{@linkplain DataSet#getParams()}而传递参数给内部<code>Freemarker</code>模板的应用场景,
+	 * 也会存在传递系统上下文变量的场景(比如传递系统当前用户)。
+	 * </p>
+	 * 
+	 * @param query         允许为{@code null}
+	 * @param dataSet
+	 * @param returnNonNull
+	 * @return 当{@code query}为{@code null}且{@code returnNonNull}为{@code false}时
+	 *         ,将返回{@code null}
+	 */
+	public DataSetQuery convert(DataSetQuery query, DataSet dataSet, boolean returnNonNull)
+	{
+		if(query == null)
+		{
+			return (returnNonNull ? DataSetQuery.valueOf() : null);
+		}
+		
+		DataSetQuery reQuery = query.copy();
+		
+		Map<String, ?> reParamValues = reQuery.getParamValues();
+		List<DataSetParam> dataSetParams = dataSet.getParams();
+		
+		reParamValues = convert(reParamValues, dataSetParams);
+		reQuery.setParamValues(reParamValues);
+		
+		return reQuery;
+	}
+
+	/**
+	 * 转换参数值映射表,返回一个经转换的新映射表。
+	 * <p>
+	 * 如果{@code paramValues}中有未在{@code dataSetParams}中定义的项,那么它将原样写入返回映射表中。
+	 * </p>
+	 * <p>
+	 * 因为对于支持<code>Freemarker</code>的{@linkplain DataSet}实现类(比如:{@linkplain SqlDataSet}),
+	 * 存在不定义{@linkplain DataSet#getParams()}而传递参数给内部<code>Freemarker</code>模板的应用场景,
+	 * 也会存在传递系统上下文变量的场景(比如传递系统当前用户)。
+	 * </p>
+	 */
+	@Override
+	public Map<String, Object> convert(Map<String, ?> paramValues, Collection<? extends DataNameType> dataSetParams)
+			throws DataValueConvertionException
+	{
+		return super.convert(paramValues, dataSetParams);
+	}
+
+	@Override
+	protected Object convertValue(Object value, String type) throws DataValueConvertionException
+	{
+		if (DataType.STRING.equals(type))
+			return convertToString(value, DataType.STRING);
+		else if (DataType.NUMBER.equals(type))
+			return convertToNumber(value, DataType.NUMBER);
+		else if (DataType.BOOLEAN.equals(type))
+			return convertToBoolean(value, DataType.BOOLEAN);
+		else
+			return convertExt(value, type);
+	}
+}

+ 41 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetParamValueRequiredException.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import org.datagear.analysis.DataSetException;
+
+/**
+ * 数据集参数值必填异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetParamValueRequiredException extends DataSetException
+{
+	private static final long serialVersionUID = 1L;
+
+	public DataSetParamValueRequiredException()
+	{
+		super();
+	}
+
+	public DataSetParamValueRequiredException(String message)
+	{
+		super(message);
+	}
+
+	public DataSetParamValueRequiredException(Throwable cause)
+	{
+		super(cause);
+	}
+
+	public DataSetParamValueRequiredException(String message, Throwable cause)
+	{
+		super(message, cause);
+	}
+}

+ 58 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetPropertyNotFoundException.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+
+/**
+ * 指定名称的{@linkplain DataSetProperty}未找到异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetPropertyNotFoundException extends DataSetException
+{
+	private static final long serialVersionUID = 1L;
+
+	private String name;
+
+	public DataSetPropertyNotFoundException(String name)
+	{
+		super("Property [" + name + "] not found");
+		this.name = name;
+	}
+
+	public DataSetPropertyNotFoundException(String name, String message)
+	{
+		super(message);
+		this.name = name;
+	}
+
+	public DataSetPropertyNotFoundException(String name, Throwable cause)
+	{
+		super(cause);
+		this.name = name;
+	}
+
+	public DataSetPropertyNotFoundException(String name, String message, Throwable cause)
+	{
+		super(message, cause);
+		this.name = name;
+	}
+
+	public String getName()
+	{
+		return name;
+	}
+
+	protected void setName(String name)
+	{
+		this.name = name;
+	}
+}

+ 258 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetPropertyValueConverter.java

@@ -0,0 +1,258 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetProperty.DataType;
+
+/**
+ * {@linkplain DataSetProperty}值转换器。
+ * <p>
+ * 它支持将对象转换为{@linkplain DataSetProperty.DataType}类型的值。
+ * </p>
+ * <p>
+ * 此类的{@linkplain #convert(java.util.Map, java.util.Collection)}、{@linkplain #convert(Object, String)}不是线程安全的。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetPropertyValueConverter extends DataValueConverter
+{
+	private DataFormat dataFormat;
+
+	private SimpleDateFormat _dateFormat = null;
+	private SimpleDateFormat _timeFormat = null;
+	private SimpleDateFormat _timestampFormat = null;
+	private DecimalFormat _numberFormat = null;
+
+	public DataSetPropertyValueConverter()
+	{
+		super();
+		setDataFormat(new DataFormat());
+	}
+
+	public DataSetPropertyValueConverter(DataFormat dataFormat)
+	{
+		super();
+		setDataFormat(dataFormat);
+	}
+
+	public DataFormat getDataFormat()
+	{
+		return dataFormat;
+	}
+
+	public void setDataFormat(DataFormat dataFormat)
+	{
+		this.dataFormat = dataFormat;
+
+		this._dateFormat = new SimpleDateFormat(dataFormat.getDateFormat());
+		this._timeFormat = new SimpleDateFormat(dataFormat.getTimeFormat());
+		this._timestampFormat = new SimpleDateFormat(dataFormat.getTimestampFormat());
+		this._numberFormat = new DecimalFormat(dataFormat.getNumberFormat());
+	}
+
+	@Override
+	protected Object convertValue(Object value, String type) throws DataValueConvertionException
+	{
+		if (value == null)
+			return null;
+
+		if (type == null)
+			return value;
+
+		try
+		{
+			if (value instanceof String)
+				return convertStringValue((String) value, type);
+			else if (value instanceof Boolean)
+				return convertBooleanValue((Boolean) value, type);
+			else if (value instanceof Number)
+				return convertNumberValue((Number) value, type);
+			else if (value instanceof Time)
+				return convertTimeValue((Time) value, type);
+			else if (value instanceof Timestamp)
+				return convertTimestampValue((Timestamp) value, type);
+			else if (value instanceof java.util.Date)
+				return convertDateValue((java.util.Date) value, type);
+			else
+			{
+				if (DataType.UNKNOWN.equals(type))
+					return value;
+				else
+					throw new DataValueConvertionException(value, type);
+			}
+		}
+		catch (DataValueConvertionException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataValueConvertionException(value, type);
+		}
+	}
+
+	protected Object convertStringValue(String value, String type) throws Throwable
+	{
+		if (DataType.STRING.equals(type) || DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null || value.isEmpty())
+			return null;
+
+		if (DataType.BOOLEAN.equals(type))
+			return "true".equalsIgnoreCase(value) || "1".equals(value);
+		else if (DataType.NUMBER.equals(type))
+			return this._numberFormat.parse(value);
+		else if (DataType.INTEGER.equals(type))
+			return this._numberFormat.parse(value).intValue();
+		else if (DataType.DECIMAL.equals(type))
+			return this._numberFormat.parse(value).doubleValue();
+		else if (DataType.DATE.equals(type))
+		{
+			java.util.Date date = convertToDateWithInteger(value, this._dateFormat);
+			return new Date(date.getTime());
+		}
+		else if (DataType.TIME.equals(type))
+		{
+			java.util.Date date = convertToDateWithInteger(value, this._timeFormat);
+			return new Time(date.getTime());
+		}
+		else if (DataType.TIMESTAMP.equals(type))
+		{
+			java.util.Date date = convertToDateWithInteger(value, this._timestampFormat);
+			return new Timestamp(date.getTime());
+		}
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+
+	protected Object convertBooleanValue(Boolean value, String type) throws Throwable
+	{
+		if (DataType.BOOLEAN.equals(type) || DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null)
+			return null;
+
+		if (DataType.STRING.equals(type))
+			return value.toString();
+		else if (DataType.NUMBER.equals(type) || DataType.INTEGER.equals(type) || DataType.DECIMAL.equals(type))
+			return (Boolean.TRUE.equals(value) ? 1 : 0);
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+
+	protected Object convertNumberValue(Number value, String type) throws Throwable
+	{
+		if (DataType.NUMBER.equals(type) || DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null)
+			return null;
+
+		if (DataType.STRING.equals(type))
+			return this._numberFormat.format(value);
+		else if (DataType.BOOLEAN.equals(type))
+			return (value.intValue() > 0);
+		else if (DataType.INTEGER.equals(type))
+			return value.longValue();
+		else if (DataType.DECIMAL.equals(type))
+			return value.doubleValue();
+		else if (DataType.DATE.equals(type))
+			return new Date(value.longValue());
+		else if (DataType.TIME.equals(type))
+			return new Time(value.longValue());
+		else if (DataType.TIMESTAMP.equals(type))
+			return new Timestamp(value.longValue());
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+
+	protected Object convertDateValue(java.util.Date value, String type) throws Throwable
+	{
+		if (DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null)
+			return null;
+
+		if (DataType.STRING.equals(type))
+			return this._dateFormat.format(value);
+		else if (DataType.NUMBER.equals(type))
+			return value.getTime();
+		else if (DataType.INTEGER.equals(type))
+			return value.getTime();
+		else if (DataType.DECIMAL.equals(type))
+			return value.getTime();
+		else if (DataType.DATE.equals(type))
+			return new Date(value.getTime());
+		else if (DataType.TIME.equals(type))
+			return new Time(value.getTime());
+		else if (DataType.TIMESTAMP.equals(type))
+			return new Timestamp(value.getTime());
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+
+	protected Object convertTimeValue(Time value, String type) throws Throwable
+	{
+		if (DataType.TIME.equals(type) || DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null)
+			return null;
+
+		if (DataType.STRING.equals(type))
+			return this._timeFormat.format(value);
+		else if (DataType.NUMBER.equals(type))
+			return value.getTime();
+		else if (DataType.INTEGER.equals(type))
+			return value.getTime();
+		else if (DataType.DECIMAL.equals(type))
+			return value.getTime();
+		else if (DataType.DATE.equals(type))
+			return new Date(value.getTime());
+		else if (DataType.TIMESTAMP.equals(type))
+			return new Timestamp(value.getTime());
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+
+	protected Object convertTimestampValue(Timestamp value, String type) throws Throwable
+	{
+		if (DataType.TIMESTAMP.equals(type) || DataType.UNKNOWN.equals(type))
+			return value;
+
+		if (value == null)
+			return null;
+
+		if (DataType.STRING.equals(type))
+			return this._timestampFormat.format(value);
+		else if (DataType.NUMBER.equals(type))
+			return value.getTime();
+		else if (DataType.INTEGER.equals(type))
+			return value.getTime();
+		else if (DataType.DECIMAL.equals(type))
+			return value.getTime();
+		else if (DataType.DATE.equals(type))
+			return new Date(value.getTime());
+		else if (DataType.TIME.equals(type))
+			return new Time(value.getTime());
+		else
+			throw new DataValueConvertionException(value, type);
+	}
+}

+ 59 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataSetSourceParseException.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import org.datagear.analysis.DataSetException;
+
+/**
+ * 数据集源解析异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataSetSourceParseException extends DataSetException
+{
+	private static final long serialVersionUID = 1L;
+
+	private String source = null;
+
+	public DataSetSourceParseException()
+	{
+		super();
+	}
+
+	public DataSetSourceParseException(String message)
+	{
+		super(message);
+	}
+
+	public DataSetSourceParseException(Throwable cause)
+	{
+		super(cause);
+	}
+
+	public DataSetSourceParseException(String message, Throwable cause)
+	{
+		super(message, cause);
+	}
+
+	public DataSetSourceParseException(Throwable cause, String source)
+	{
+		super(cause);
+		this.source = source;
+	}
+
+	public String getSource()
+	{
+		return source;
+	}
+
+	public void setSource(String source)
+	{
+		this.source = source;
+	}
+}

+ 272 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataValueConverter.java

@@ -0,0 +1,272 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.datagear.analysis.DataNameType;
+
+/**
+ * 数据值转换器。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public abstract class DataValueConverter
+{
+	/** 正则表达式:小数 */
+	public static final Pattern PATTERN_DECIMAL_NUMBER = Pattern.compile("^[^\\.]+\\.[^\\.]+$");
+
+	/** 正则表达式:整数 */
+	public static final Pattern PATTERN_INTEGER = Pattern.compile("^-?[1-9]\\d*$");
+
+	/**
+	 * 转换数据值映射表,返回一个经转换的新映射表。
+	 * <p>
+	 * 如果{@code nameValues}中有未在{@code dataNameTypes}中定义的项,那么它将原样写入返回映射表中。
+	 * </p>
+	 * 
+	 * @param nameValues
+	 *            原始名/值映射表,允许为{@code null}
+	 * @param dataNameTypes
+	 *            名/类型集合,允许为{@code null}
+	 * @return
+	 * @throws DataValueConvertionException
+	 */
+	public Map<String, Object> convert(Map<String, ?> nameValues, Collection<? extends DataNameType> dataNameTypes)
+			throws DataValueConvertionException
+	{
+		if (nameValues == null)
+			return null;
+
+		Map<String, Object> re = new HashMap<>(nameValues);
+
+		if (dataNameTypes != null)
+		{
+			for (DataNameType dnt : dataNameTypes)
+			{
+				String name = dnt.getName();
+
+				if (!nameValues.containsKey(name))
+					continue;
+
+				Object value = nameValues.get(name);
+				value = convert(value, dnt.getType());
+
+				re.put(name, value);
+			}
+		}
+
+		return re;
+	}
+
+	/**
+	 * 转换数据值。
+	 * 
+	 * @param value
+	 *            待转换的数据值、数据值数组、数据值集合。
+	 * @param type
+	 *            目标类型
+	 * @return 转换结果对象,当{@code value}是数组时,返回{@code Object[]};当{@code value}是{@linkplain Collection}时,返回{@linkplain List}。
+	 * @throws DataValueConvertionException
+	 */
+	public Object convert(Object value, String type) throws DataValueConvertionException
+	{
+		if (value == null)
+		{
+			return convertValue(value, type);
+		}
+		else if (value instanceof Object[])
+		{
+			Object[] src = (Object[]) value;
+			return convertArray(src, type);
+		}
+		else if (value instanceof Collection<?>)
+		{
+			@SuppressWarnings("unchecked")
+			Collection<Object> src = (Collection<Object>) value;
+			return convertCollection(src, type);
+		}
+		else
+			return convertValue(value, type);
+	}
+
+	protected Object convertArray(Object[] values, String type) throws DataValueConvertionException
+	{
+		if (values == null)
+			throw new IllegalArgumentException("[values] must not be null");
+
+		Object[] target = new Object[values.length];
+
+		for (int i = 0; i < values.length; i++)
+		{
+			target[i] = convertValue(values[i], type);
+		}
+
+		return target;
+	}
+
+	protected Object convertCollection(Collection<?> values, String type) throws DataValueConvertionException
+	{
+		if (values == null)
+			throw new IllegalArgumentException("[values] must not be null");
+
+		List<Object> target = new ArrayList<>(values.size());
+
+		for (Object ele : values)
+		{
+			target.add(convertValue(ele, type));
+		}
+
+		return target;
+	}
+
+	/**
+	 * 转换数据值。
+	 * 
+	 * @param value
+	 *            要转换的数据值,不会是数组,可能为{@code null}
+	 * @param type
+	 * @return
+	 * @throws DataValueConvertionException
+	 */
+	protected abstract Object convertValue(Object value, String type) throws DataValueConvertionException;
+
+	protected Number convertToNumber(Object value, String numberType)
+	{
+		if (value == null)
+			return null;
+
+		if (value instanceof Number)
+			return (Number) value;
+
+		if (value instanceof String)
+		{
+			String str = (String) value;
+
+			if (str.isEmpty())
+				return null;
+
+			try
+			{
+				if (isDecimalNumberString(str))
+					return Double.valueOf(str);
+				else
+				{
+					Long re = Long.valueOf(str);
+
+					if (re <= Integer.MAX_VALUE && re >= Integer.MIN_VALUE)
+						return re.intValue();
+					else
+						return re.longValue();
+				}
+			}
+			catch (NumberFormatException e)
+			{
+				throw new DataValueConvertionException(value, numberType, e);
+			}
+		}
+
+		return (Number) convertExt(value, numberType);
+	}
+
+	protected String convertToString(Object value, String stringType)
+	{
+		if (value == null)
+			return null;
+
+		if (value instanceof String)
+			return (String) value;
+
+		return value.toString();
+	}
+
+	protected Boolean convertToBoolean(Object value, String booleanType)
+	{
+		if (value == null)
+			return null;
+
+		if (value instanceof Boolean)
+			return (Boolean) value;
+
+		if (value instanceof String)
+		{
+			String str = (String) value;
+
+			if (str.isEmpty())
+				return null;
+			else
+				return str.equalsIgnoreCase("true") || str.equals("1");
+		}
+
+		return (Boolean) convertExt(value, booleanType);
+	}
+
+	protected Object convertExt(Object value, String type) throws DataValueConvertionException
+	{
+		throw new DataValueConvertionException(value, type,
+				"Convert [" + value + "] to type [" + type + "] is not supported");
+	}
+
+	/**
+	 * 将字符串转换为日期。
+	 * <p>
+	 * 如果{@code str}不匹配{@code format},但又匹配整数的话,将按照毫秒数转换处理。
+	 * </p>
+	 * 
+	 * @param str
+	 * @param format
+	 * @return
+	 * @throws ParseException
+	 */
+	protected java.util.Date convertToDateWithInteger(String str, SimpleDateFormat format) throws ParseException
+	{
+		if(str == null || str.isEmpty())
+			return null;
+		
+		try
+		{
+			return format.parse(str);
+		}
+		catch(ParseException e)
+		{
+			// 是整数
+			if (isIntegerString(str))
+			{
+				try
+				{
+					long time = Long.valueOf(str);
+					return new java.util.Date(time);
+				}
+				catch (NumberFormatException e1)
+				{
+					throw e;
+				}
+			}
+			else
+				throw e;
+		}
+	}
+
+	protected boolean isDecimalNumberString(String str)
+	{
+		return PATTERN_DECIMAL_NUMBER.matcher(str).matches();
+	}
+
+	protected boolean isIntegerString(String str)
+	{
+		return PATTERN_INTEGER.matcher(str).matches();
+	}
+}

+ 72 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DataValueConvertionException.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+/**
+ * 数据值转换异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DataValueConvertionException extends RuntimeException
+{
+	private static final long serialVersionUID = 1L;
+
+	private Object source;
+
+	private String type;
+
+	public DataValueConvertionException(Object source, String type)
+	{
+		super("Convert from [" + source + "] to [" + type + "] is not supported");
+		this.type = type;
+		this.source = source;
+	}
+
+	public DataValueConvertionException(Object source, String type, String message)
+	{
+		super(message);
+		this.type = type;
+		this.source = source;
+	}
+
+	public DataValueConvertionException(Object source, String type, Throwable cause)
+	{
+		super(cause);
+		this.type = type;
+		this.source = source;
+	}
+
+	public DataValueConvertionException(Object source, String type, String message, Throwable cause)
+	{
+		super(message, cause);
+		this.type = type;
+		this.source = source;
+	}
+
+	public Object getSource()
+	{
+		return source;
+	}
+
+	protected void setSource(Object source)
+	{
+		this.source = source;
+	}
+
+	public String getType()
+	{
+		return type;
+	}
+
+	protected void setType(String type)
+	{
+		this.type = type;
+	}
+
+}

+ 81 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/DefaultRenderContext.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.datagear.analysis.RenderContext;
+
+/**
+ * 抽象{@linkplain RenderContext}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class DefaultRenderContext implements RenderContext
+{
+	private Map<String, Object> attributes = new HashMap<>();
+
+	public DefaultRenderContext()
+	{
+		super();
+	}
+
+	public DefaultRenderContext(Map<String, ?> attributes)
+	{
+		super();
+		this.attributes.putAll(attributes);
+	}
+
+	public DefaultRenderContext(RenderContext renderContext)
+	{
+		super();
+
+		Map<String, ?> attributes = renderContext.getAttributes();
+		if (attributes != null)
+			this.attributes.putAll(attributes);
+	}
+
+	public void setAttributes(Map<String, Object> attributes)
+	{
+		this.attributes = attributes;
+	}
+
+	@Override
+	public Map<String, ?> getAttributes()
+	{
+		return this.attributes;
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T> T getAttribute(String name)
+	{
+		return (T) this.attributes.get(name);
+	}
+
+	@Override
+	public void setAttribute(String name, Object value)
+	{
+		this.attributes.put(name, value);
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T> T removeAttribute(String name)
+	{
+		return (T) this.attributes.remove(name);
+	}
+
+	@Override
+	public boolean hasAttribute(String name)
+	{
+		return this.attributes.containsKey(name);
+	}
+}

+ 82 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ErrorMessageDashboardResult.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.datagear.analysis.ChartResultError;
+import org.datagear.analysis.DashboardResult;
+
+/**
+ * 错误消息的{@linkplain DashboardResult}。
+ * <p>
+ * 它的{@linkplain #getChartResultErrors()}始终返回空映射表,
+ * {@linkplain #getChartResultErrorMessages()}则是由{@linkplain DashboardResult#getChartResultErrors()}转换而得。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ErrorMessageDashboardResult extends DashboardResult
+{
+	private Map<String, ChartResultErrorMessage> chartResultErrorMessages = Collections.emptyMap();
+
+	public ErrorMessageDashboardResult(DashboardResult result)
+	{
+		super.setChartResults(result.getChartResults());
+		setChartResultErrorMessages(toChartResultErrorMessages(result.getChartResultErrors(), false));
+	}
+
+	public ErrorMessageDashboardResult(DashboardResult result, boolean rootCauseMessage)
+	{
+		super.setChartResults(result.getChartResults());
+		setChartResultErrorMessages(toChartResultErrorMessages(result.getChartResultErrors(), rootCauseMessage));
+	}
+
+	public Map<String, ChartResultErrorMessage> getChartResultErrorMessages()
+	{
+		return chartResultErrorMessages;
+	}
+
+	protected void setChartResultErrorMessages(Map<String, ChartResultErrorMessage> chartResultErrorMessages)
+	{
+		this.chartResultErrorMessages = chartResultErrorMessages;
+	}
+
+	@Override
+	public Map<String, ChartResultError> getChartResultErrors()
+	{
+		return Collections.emptyMap();
+	}
+
+	@Override
+	public void setChartResultErrors(Map<String, ChartResultError> chartResultErrors)
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	protected Map<String, ChartResultErrorMessage> toChartResultErrorMessages(
+			Map<String, ChartResultError> chartResultErrors, boolean rootCauseMessage)
+	{
+		if (chartResultErrors == null)
+			return Collections.emptyMap();
+
+		Map<String, ChartResultErrorMessage> re = new HashMap<String, ChartResultErrorMessage>(
+				chartResultErrors.size());
+
+		for (Map.Entry<String, ChartResultError> entry : chartResultErrors.entrySet())
+		{
+			ChartResultErrorMessage em = new ChartResultErrorMessage(entry.getValue(), rootCauseMessage);
+			re.put(entry.getKey(), em);
+		}
+
+		return re;
+	}
+}

+ 89 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.util.List;
+
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.FileUtil;
+
+/**
+ * 目录内Excel文件{@linkplain DataSet}。
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ExcelDirectoryFileDataSet extends AbstractExcelDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** Excel文件所在的目录 */
+	private File directory;
+
+	/** Excel文件名 */
+	private String fileName;
+
+	public ExcelDirectoryFileDataSet()
+	{
+		super();
+	}
+
+	public ExcelDirectoryFileDataSet(String id, String name, File directory, String fileName)
+	{
+		super(id, name);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	/**
+	 * @param id
+	 * @param name
+	 * @param properties
+	 */
+	public ExcelDirectoryFileDataSet(String id, String name, List<DataSetProperty> properties, File directory,
+			String fileName)
+	{
+		super(id, name, properties);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	public File getDirectory()
+	{
+		return directory;
+	}
+
+	public void setDirectory(File directory)
+	{
+		this.directory = directory;
+	}
+
+	public String getFileName()
+	{
+		return fileName;
+	}
+
+	public void setFileName(String fileName)
+	{
+		this.fileName = fileName;
+	}
+
+	@Override
+	protected File getExcelFile(DataSetQuery query) throws DataSetException
+	{
+		File excelFile = FileUtil.getFile(this.directory, this.fileName);
+		return excelFile;
+	}
+}

+ 206 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/FileTemplateDashboardWidgetResManager.java

@@ -0,0 +1,206 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import org.datagear.analysis.Dashboard;
+import org.datagear.analysis.TemplateDashboardWidgetResManager;
+import org.datagear.util.FileUtil;
+import org.datagear.util.IOUtil;
+
+/**
+ * 基于文件的{@linkplain TemplateDashboardWidgetResManager}。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class FileTemplateDashboardWidgetResManager extends AbstractTemplateDashboardWidgetResManager
+{
+	private File rootDirectory;
+
+	public FileTemplateDashboardWidgetResManager()
+	{
+		super();
+	}
+
+	public FileTemplateDashboardWidgetResManager(File rootDirectory)
+	{
+		super();
+		this.rootDirectory = rootDirectory;
+	}
+
+	public FileTemplateDashboardWidgetResManager(String rootDirectory)
+	{
+		super();
+		this.rootDirectory = FileUtil.getDirectory(FileUtil.trimPath(rootDirectory));
+	}
+
+	public File getRootDirectory()
+	{
+		return rootDirectory;
+	}
+
+	public void setRootDirectory(File rootDirectory)
+	{
+		this.rootDirectory = rootDirectory;
+
+		if (!this.rootDirectory.exists())
+			this.rootDirectory.mkdirs();
+	}
+
+	/**
+	 * 获取指定资源相对{@linkplain #getRootDirectory()}的路径。
+	 * 
+	 * @param id
+	 *            {@linkplain Dashboard#getId()}
+	 * @param name
+	 *            资源名称
+	 * @return
+	 */
+	public String getRelativePath(String id, String name)
+	{
+		return doGetRelativePath(id, name);
+	}
+
+	@Override
+	public boolean exists(String id, String name)
+	{
+		File file = getFile(id, name, false);
+		return file.exists();
+	}
+
+	@Override
+	public InputStream getInputStream(String id, String name) throws IOException
+	{
+		File file = getFile(id, name, false);
+		return IOUtil.getInputStream(file);
+	}
+
+	@Override
+	public OutputStream getOutputStream(String id, String name) throws IOException
+	{
+		File file = getFile(id, name, true);
+		return IOUtil.getOutputStream(file);
+	}
+
+	@Override
+	public void copyFrom(String id, File directory) throws IOException
+	{
+		File myDirectory = FileUtil.getDirectory(this.rootDirectory, id);
+		IOUtil.copy(directory, myDirectory, false);
+	}
+
+	@Override
+	public void copyTo(String id, File directory) throws IOException
+	{
+		File myDirectory = FileUtil.getDirectory(this.rootDirectory, id);
+		IOUtil.copy(myDirectory, directory, false);
+	}
+
+	@Override
+	public void copyTo(String sourceId, String targetId) throws IOException
+	{
+		File sourceDirectory = FileUtil.getDirectory(this.rootDirectory, sourceId);
+		File targetDirectory = FileUtil.getDirectory(this.rootDirectory, targetId);
+
+		IOUtil.copy(sourceDirectory, targetDirectory, false);
+	}
+
+	@Override
+	public long lastModified(String id, String name)
+	{
+		File file = getFile(id, name, false);
+		return file.lastModified();
+	}
+
+	@Override
+	public List<String> list(String id)
+	{
+		File directory = FileUtil.getDirectory(this.rootDirectory, id, false);
+
+		if (!directory.exists())
+			return new ArrayList<>(0);
+
+		List<File> files = new ArrayList<>();
+		listAllDescendentFiles(directory, files);
+
+		List<String> resources = new ArrayList<>(files.size());
+
+		for (File file : files)
+		{
+			String resource = FileUtil.getRelativePath(directory, file);
+
+			resource = FileUtil.trimPath(resource, FileUtil.PATH_SEPARATOR_SLASH);
+			if (file.isDirectory())
+				resource += FileUtil.PATH_SEPARATOR_SLASH;
+
+			resources.add(resource);
+		}
+
+		return resources;
+	}
+
+	@Override
+	public void delete(String id)
+	{
+		File directory = FileUtil.getDirectory(this.rootDirectory, id);
+		FileUtil.deleteFile(directory);
+	}
+
+	@Override
+	public void delete(String id, String name)
+	{
+		File file = getFile(id, name, false);
+		FileUtil.deleteFile(file);
+	}
+
+	protected void listAllDescendentFiles(File directory, List<File> files)
+	{
+		if (!directory.exists())
+			return;
+
+		File[] children = directory.listFiles();
+
+		Arrays.sort(children, new Comparator<File>()
+		{
+			@Override
+			public int compare(File o1, File o2)
+			{
+				return o1.getName().compareTo(o2.getName());
+			}
+		});
+
+		for (File child : children)
+		{
+			files.add(child);
+
+			if (child.isDirectory())
+				listAllDescendentFiles(child, files);
+		}
+	}
+
+	protected File getFile(String id, String name, boolean create)
+	{
+		String path = doGetRelativePath(id, name);
+		return FileUtil.getFile(this.rootDirectory, path, create);
+	}
+
+	protected String doGetRelativePath(String id, String name)
+	{
+		String path = FileUtil.concatPath(id, name);
+		return path;
+	}
+}

+ 39 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/HeaderContentNotNameValueObjArrayJsonException.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+/**
+ * {@linkplain HttpDataSet#getHeaderContent()}不是名/值对象数组JSON异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class HeaderContentNotNameValueObjArrayJsonException extends NotNameValueObjArrayJsonException
+{
+	private static final long serialVersionUID = 1L;
+
+	public HeaderContentNotNameValueObjArrayJsonException(String json)
+	{
+		super(json);
+	}
+
+	public HeaderContentNotNameValueObjArrayJsonException(String json, String message)
+	{
+		super(json, message);
+	}
+
+	public HeaderContentNotNameValueObjArrayJsonException(String json, Throwable cause)
+	{
+		super(json, cause);
+	}
+
+	public HeaderContentNotNameValueObjArrayJsonException(String json, String message, Throwable cause)
+	{
+		super(json, message, cause);
+	}
+}

+ 685 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/HttpDataSet.java

@@ -0,0 +1,685 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hc.client5.http.HttpResponseException;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpDelete;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPatch;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.DataSetResult;
+import org.datagear.analysis.ResolvedDataSetResult;
+import org.datagear.util.IOUtil;
+import org.datagear.util.StringUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * HTTP数据集。
+ * <p>
+ * 此类的{@linkplain #getUri()}、{@linkplain #getHeaderContent()}、{@linkplain #getRequestContent()}支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class HttpDataSet extends AbstractResolvableDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	protected static final Logger LOGGER = LoggerFactory.getLogger(HttpDataSet.class);
+
+	public static final String REQUEST_METHOD_GET = "GET";
+
+	public static final String REQUEST_METHOD_POST = "POST";
+
+	public static final String REQUEST_METHOD_PUT = "PUT";
+
+	public static final String REQUEST_METHOD_PATCH = "PATCH";
+
+	public static final String REQUEST_METHOD_DELETE = "DELETE";
+
+	// 这些HTTP方法不适应于此
+	// public static final String REQUEST_METHOD_HEAD = "HEAD";
+	// public static final String REQUEST_METHOD_OPTIONS = "OPTIONS";
+	// public static final String REQUEST_METHOD_TRACE = "TRACE";
+
+	/**
+	 * 请求内容类型:表单式的参数名/值类型,对应的HTTP请求类型为:application/x-www-form-urlencoded
+	 */
+	public static final String REQUEST_CONTENT_TYPE_FORM_URLENCODED = "FORM_URLENCODED";
+
+	/**
+	 * 请求内容类型:JSON,对应的HTTP请求类型为:application/json
+	 */
+	public static final String REQUEST_CONTENT_TYPE_JSON = "JSON";
+
+	/**
+	 * 响应内容类型:JSON,对应的HTTP响应类型为:application/json
+	 */
+	public static final String RESPONSE_CONTENT_TYPE_JSON = "JSON";
+
+	protected static final List<NameValuePair> NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON = new ArrayList<>(0);
+
+	/** HTTP客户端 */
+	private transient HttpClient httpClient;
+
+	/** HTTP请求地址 */
+	private String uri;
+
+	/** 请求头JSON文本 */
+	private String headerContent = "";
+
+	/** 请求方法 */
+	private String requestMethod = REQUEST_METHOD_GET;
+
+	/** 请求内容类型 */
+	private String requestContentType = REQUEST_CONTENT_TYPE_FORM_URLENCODED;
+
+	/** 请求内容编码 */
+	private String requestContentCharset = IOUtil.CHARSET_UTF_8;
+
+	/** 请求内容JSON文本 */
+	private String requestContent = "";
+
+	/** 响应类型 */
+	private String responseContentType = RESPONSE_CONTENT_TYPE_JSON;
+
+	/** 响应数据的JSON路径 */
+	private String responseDataJsonPath = "";
+
+	public HttpDataSet()
+	{
+		super();
+	}
+
+	public HttpDataSet(String id, String name, HttpClient httpClient, String uri)
+	{
+		super(id, name);
+		this.httpClient = httpClient;
+		this.uri = uri;
+	}
+
+	public HttpDataSet(String id, String name, List<DataSetProperty> properties, HttpClient httpClient, String uri)
+	{
+		super(id, name, properties);
+		this.httpClient = httpClient;
+		this.uri = uri;
+	}
+
+	public HttpClient getHttpClient()
+	{
+		return httpClient;
+	}
+
+	public void setHttpClient(HttpClient httpClient)
+	{
+		this.httpClient = httpClient;
+	}
+
+	public String getUri()
+	{
+		return uri;
+	}
+
+	/**
+	 * 设置请求地址。
+	 * <p>
+	 * 请求地址支持<code>Freemarker</code>模板语言。
+	 * </p>
+	 * 
+	 * @param uri
+	 */
+	public void setUri(String uri)
+	{
+		this.uri = uri;
+	}
+
+	public String getHeaderContent()
+	{
+		return headerContent;
+	}
+
+	/**
+	 * 设置请求头JSON文本,格式应为:
+	 * <p>
+	 * <code>
+	 * <pre>
+	 * [
+	 *   {name: "...", value: "..."},
+	 *   {name: "...", value: "..."},
+	 *   ...
+	 * ]
+	 * </pre>
+	 * </code>
+	 * </p>
+	 * <p>
+	 * 请求头JSON文本支持<code>Freemarker</code>模板语言。
+	 * </p>
+	 * 
+	 * @param headerContent
+	 */
+	public void setHeaderContent(String headerContent)
+	{
+		this.headerContent = headerContent;
+	}
+
+	public String getRequestMethod()
+	{
+		return requestMethod;
+	}
+
+	/**
+	 * 设置HTTP方法,参考{@code REQUEST_METHOD_*}常量。
+	 * 
+	 * @param requestMethod
+	 */
+	public void setRequestMethod(String requestMethod)
+	{
+		this.requestMethod = requestMethod;
+	}
+
+	public String getRequestContentType()
+	{
+		return requestContentType;
+	}
+
+	/**
+	 * 设置请求内容类型,允许的值为:
+	 * <p>
+	 * {@linkplain #REQUEST_CONTENT_TYPE_FORM_URLENCODED}、{@linkplain #REQUEST_CONTENT_TYPE_JSON}。
+	 * </p>
+	 * 
+	 * @param requestContentType
+	 */
+	public void setRequestContentType(String requestContentType)
+	{
+		this.requestContentType = requestContentType;
+	}
+
+	public String getRequestContentCharset()
+	{
+		return requestContentCharset;
+	}
+
+	/**
+	 * 设置请求内容编码。
+	 * <p>
+	 * 默认请求内容编码为{@code UTF-8}。
+	 * </p>
+	 * 
+	 * @param requestContentCharset
+	 */
+	public void setRequestContentCharset(String requestContentCharset)
+	{
+		this.requestContentCharset = requestContentCharset;
+	}
+
+	public String getRequestContent()
+	{
+		return requestContent;
+	}
+
+	/**
+	 * 设置请求内容JSON文本,为{@code null}或{@code ""}表示无请求内容。
+	 * <p>
+	 * 当{@linkplain #getRequestContentType()}为{@linkplain #REQUEST_CONTENT_TYPE_FORM_URLENCODED}时,请求内容JSON文本格式应为:
+	 * </p>
+	 * <code>
+	 * <pre>
+	 * [
+	 *   {name: "...", value: "..."},
+	 *   {name: "...", value: "..."},
+	 *   ...
+	 * ]
+	 * </pre>
+	 * </code>
+	 * <p>
+	 * 其中,{@code name}表示请求参数名,{@code value}表示请求参数值。
+	 * </p>
+	 * <p>
+	 * 当{@linkplain #getRequestContentType()}为{@linkplain #REQUEST_CONTENT_TYPE_JSON}时,请求内容JSON文本没有特殊格式要求。
+	 * </p>
+	 * <p>
+	 * 请求内容JSON文本支持<code>Freemarker</code>模板语言。
+	 * </p>
+	 * 
+	 * @param requestContent
+	 */
+	public void setRequestContent(String requestContent)
+	{
+		this.requestContent = requestContent;
+	}
+
+	public String getResponseContentType()
+	{
+		return responseContentType;
+	}
+
+	/**
+	 * 设置相应类型。
+	 * <p>
+	 * 目前仅支持{@linkplain #RESPONSE_CONTENT_TYPE_JSON},且是默认值。
+	 * </p>
+	 * 
+	 * @param responseContentType
+	 */
+	public void setResponseContentType(String responseContentType)
+	{
+		this.responseContentType = responseContentType;
+	}
+
+	public String getResponseDataJsonPath()
+	{
+		return responseDataJsonPath;
+	}
+
+	/**
+	 * 设置响应数据的JSON路径。
+	 * <p>
+	 * 当希望返回的是响应原始JSON数据的指定JSON路径值时,可以设置此项。
+	 * </p>
+	 * <p>
+	 * 具体格式参考{@linkplain AbstractJsonDataSet#setDataJsonPath(String)}。
+	 * </p>
+	 * <p>
+	 * 默认无数据路径,将直接返回响应原始JSON数据。
+	 * </p>
+	 * 
+	 * @param responseDataJsonPath
+	 */
+	public void setResponseDataJsonPath(String responseDataJsonPath)
+	{
+		this.responseDataJsonPath = responseDataJsonPath;
+	}
+
+	@Override
+	public TemplateResolvedDataSetResult resolve(DataSetQuery query)
+			throws DataSetException
+	{
+		return (TemplateResolvedDataSetResult) super.resolve(query);
+	}
+
+	@Override
+	protected TemplateResolvedDataSetResult resolveResult(DataSetQuery query, List<DataSetProperty> properties,
+			boolean resolveProperties) throws DataSetException
+	{
+		String uri = null;
+		String headerContent = null;
+		String requestContent = null;
+
+		try
+		{
+			uri = resolveTemplateUri(query);
+			headerContent = resolveTemplateHeaderContent(query);
+			requestContent = resolveTemplateRequestContent(query);
+
+			ClassicHttpRequest request = createHttpRequest(uri);
+
+			setHttpHeaders(request, headerContent);
+			setHttpEntity(request, requestContent);
+
+			JsonResponseHandler responseHandler = new JsonResponseHandler();
+			responseHandler.setProperties(properties);
+			responseHandler.setResponseDataJsonPath(getResponseDataJsonPath());
+			responseHandler.setDataSetQuery(query);
+
+			ResolvedDataSetResult result = this.httpClient.execute(request, responseHandler);
+
+			return new TemplateResolvedDataSetResult(result.getResult(), result.getProperties(),
+					buildResolvedTemplate(uri, headerContent, requestContent));
+		}
+		catch (DataSetException e)
+		{
+			throw e;
+		}
+		catch (Throwable t)
+		{
+			throw new DataSetSourceParseException(t, buildResolvedTemplate(uri, headerContent, requestContent));
+		}
+	}
+
+	protected String buildResolvedTemplate(String uri, String headerContent, String requestContent)
+	{
+		StringBuilder sb = new StringBuilder();
+
+		if (!StringUtil.isEmpty(uri))
+		{
+			if (sb.length() > 0)
+				sb.append(
+						System.lineSeparator() + "-----------------------------------------" + System.lineSeparator());
+
+			sb.append("URI:" + System.lineSeparator() + uri);
+		}
+
+		if (!StringUtil.isEmpty(headerContent))
+		{
+			if (sb.length() > 0)
+				sb.append(
+						System.lineSeparator() + "-----------------------------------------" + System.lineSeparator());
+
+			sb.append("Request headers:" + System.lineSeparator() + headerContent);
+		}
+
+		if (!StringUtil.isEmpty(requestContent))
+		{
+			if (sb.length() > 0)
+				sb.append(
+						System.lineSeparator() + "-----------------------------------------" + System.lineSeparator());
+
+			sb.append("Request content:" + System.lineSeparator() + requestContent);
+		}
+
+		return sb.toString();
+	}
+
+	protected void setHttpHeaders(ClassicHttpRequest request, String headerContent) throws Throwable
+	{
+		if (StringUtil.isEmpty(headerContent))
+			return;
+
+		List<NameValuePair> headers = toNameValuePairs(headerContent);
+
+		if (headers == NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON)
+			throw new HeaderContentNotNameValueObjArrayJsonException(headerContent);
+
+		for (NameValuePair header : headers)
+			request.setHeader(header.getName(), header.getValue());
+	}
+
+	protected void setHttpEntity(ClassicHttpRequest request, String requestContent) throws Throwable
+	{
+		if (REQUEST_CONTENT_TYPE_FORM_URLENCODED.equals(this.requestContentType))
+		{
+			List<NameValuePair> params = toNameValuePairs(requestContent);
+
+			if (params == NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON)
+				throw new RequestContentNotNameValueObjArrayJsonException(requestContent);
+
+			request.setEntity(new UrlEncodedFormEntity(params, Charset.forName(this.requestContentCharset)));
+		}
+		else if (REQUEST_CONTENT_TYPE_JSON.equals(this.requestContentType))
+		{
+			ContentType contentType = ContentType.create(ContentType.APPLICATION_JSON.getMimeType(),
+					Charset.forName(this.requestContentCharset));
+			StringEntity entity = new StringEntity(requestContent, contentType);
+			request.setEntity(entity);
+		}
+		else
+			throw new DataSetException("Request content type [" + this.requestContentType + "] is not supported");
+	}
+
+	protected String resolveTemplateUri(DataSetQuery query) throws Throwable
+	{
+		return resolveTextAsGeneralTemplate(this.uri, query);
+	}
+
+	protected String resolveTemplateHeaderContent(DataSetQuery query) throws Throwable
+	{
+		return resolveJsonAsTemplate(this.headerContent, query);
+	}
+
+	protected String resolveTemplateRequestContent(DataSetQuery query) throws Throwable
+	{
+		return resolveJsonAsTemplate(this.requestContent, query);
+	}
+
+	/**
+	 * 将指定JSON文本作为模板解析。
+	 * 
+	 * @param json
+	 * @param query
+	 * @return
+	 */
+	protected String resolveJsonAsTemplate(String json, DataSetQuery query)
+	{
+		return resolveTextAsTemplate(AbstractJsonDataSet.JSON_TEMPLATE_RESOLVER, json, query);
+	}
+
+	protected ClassicHttpRequest createHttpRequest(String uri) throws Throwable
+	{
+		if (REQUEST_METHOD_GET.equals(this.requestMethod) || StringUtil.isEmpty(this.requestMethod))
+			return new HttpGet(uri);
+		else if (REQUEST_METHOD_POST.equals(this.requestMethod))
+			return new HttpPost(uri);
+		else if (REQUEST_METHOD_PUT.equals(this.requestMethod))
+			return new HttpPut(uri);
+		else if (REQUEST_METHOD_PATCH.equals(this.requestMethod))
+			return new HttpPatch(uri);
+		else if (REQUEST_METHOD_DELETE.equals(this.requestMethod))
+			return new HttpDelete(uri);
+		// else if (REQUEST_METHOD_HEAD.equals(this.httpMethod))
+		// return new HttpHead(uri);
+		// else if (REQUEST_METHOD_OPTIONS.equals(this.httpMethod))
+		// return new HttpOptions(uri);
+		// else if (REQUEST_METHOD_TRACE.equals(this.httpMethod))
+		// return new HttpTrace(uri);
+		else
+			throw new DataSetException("HTTP method [" + this.requestMethod + "] is not supported");
+	}
+
+	/**
+	 * 将指定JSON字符串转换为名/值列表。
+	 * 
+	 * @param nameValueObjJsonArray
+	 *            允许为{@code null}、{@code ""}
+	 * @return 空列表表示无名/值,返回{@code #NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON}表示{@code nameValueObjJsonArray}格式不合法
+	 * @throws Throwable
+	 */
+	@SuppressWarnings("unchecked")
+	protected List<NameValuePair> toNameValuePairs(String nameValueObjJsonArray) throws Throwable
+	{
+		if (StringUtil.isEmpty(nameValueObjJsonArray))
+			return Collections.EMPTY_LIST;
+
+		Object jsonObj = getObjectMapperNonStardand().readValue(nameValueObjJsonArray, Object.class);
+
+		if (jsonObj == null)
+			return Collections.EMPTY_LIST;
+
+		if (!(jsonObj instanceof Collection<?>))
+			return NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON;
+
+		Collection<?> collection = (Collection<?>) jsonObj;
+
+		List<NameValuePair> nameValuePairs = new ArrayList<>(collection.size());
+
+		for (Object ele : collection)
+		{
+			String name = null;
+			String value = null;
+
+			if (ele instanceof Map<?, ?>)
+			{
+				Map<String, ?> eleMap = (Map<String, ?>) ele;
+				Object nameVal = eleMap.get("name");
+				Object valueVal = eleMap.get("value");
+
+				if (nameVal instanceof String)
+				{
+					name = (String) nameVal;
+					if (valueVal != null)
+						value = (valueVal instanceof String ? (String) valueVal : valueVal.toString());
+				}
+			}
+
+			if (name == null)
+				return NOT_NAME_VALUE_PAIR_OBJ_ARRAY_JSON;
+
+			nameValuePairs.add(new BasicNameValuePair(name, value));
+		}
+
+		return nameValuePairs;
+	}
+
+	protected ObjectMapper getObjectMapperNonStardand()
+	{
+		return JsonSupport.getObjectMapperNonStardand();
+	}
+
+	protected static class JsonResponseHandler implements HttpClientResponseHandler<ResolvedDataSetResult>
+	{
+		private List<DataSetProperty> properties;
+
+		private String responseDataJsonPath = "";
+
+		private DataSetQuery dataSetQuery = null;
+
+		public JsonResponseHandler()
+		{
+			super();
+		}
+
+		public List<DataSetProperty> getProperties()
+		{
+			return properties;
+		}
+
+		/**
+		 * 设置数据集属性。
+		 * 
+		 * @param properties
+		 *            如果为{@code null}或空,则执行解析
+		 */
+		public void setProperties(List<DataSetProperty> properties)
+		{
+			this.properties = properties;
+		}
+
+		public String getResponseDataJsonPath()
+		{
+			return responseDataJsonPath;
+		}
+
+		public void setResponseDataJsonPath(String responseDataJsonPath)
+		{
+			this.responseDataJsonPath = responseDataJsonPath;
+		}
+
+		public DataSetQuery getDataSetQuery()
+		{
+			return dataSetQuery;
+		}
+
+		public void setDataSetQuery(DataSetQuery dataSetQuery)
+		{
+			this.dataSetQuery = dataSetQuery;
+		}
+
+		@Override
+		public ResolvedDataSetResult handleResponse(ClassicHttpResponse response) throws HttpException, IOException
+		{
+			int code = response.getCode();
+			HttpEntity entity = response.getEntity();
+
+			if (code < 200 || code >= 300)
+				throw new HttpResponseException(code, response.getReasonPhrase());
+
+			Reader reader = null;
+
+			if (entity == null)
+				reader = IOUtil.getReader("");
+			else
+			{
+				Charset contentCharset = resolveCharset(entity, ContentType.APPLICATION_JSON.getCharset());
+				reader = IOUtil.getReader(entity.getContent(), contentCharset);
+			}
+
+			if (this.properties == null || this.properties.isEmpty())
+			{
+				HttpResponseJsonDataSet jsonDataSet = new HttpResponseJsonDataSet(reader);
+				jsonDataSet.setDataJsonPath(this.responseDataJsonPath);
+
+				return jsonDataSet.resolve(this.dataSetQuery);
+			}
+			else
+			{
+				HttpResponseJsonDataSet jsonDataSet = new HttpResponseJsonDataSet(this.properties, reader);
+				jsonDataSet.setDataJsonPath(this.responseDataJsonPath);
+
+				DataSetResult result = jsonDataSet.getResult(this.dataSetQuery);
+				return new ResolvedDataSetResult(result, this.properties);
+			}
+		}
+
+		protected Charset resolveCharset(HttpEntity entity, Charset defaultCharset)
+		{
+			Charset contentCharset = null;
+
+			String contentTypeStr = entity.getContentType();
+
+			if (!StringUtil.isEmpty(contentTypeStr))
+			{
+				try
+				{
+					ContentType contentType = ContentType.parse(contentTypeStr);
+					contentCharset = contentType.getCharset();
+				}
+				catch (Throwable t)
+				{
+					LOGGER.warn("Default charset [" + defaultCharset + "] will be used because parse error", t);
+
+					contentCharset = defaultCharset;
+				}
+			}
+
+			return (contentCharset != null ? contentCharset : defaultCharset);
+		}
+	}
+
+	protected static class HttpResponseJsonDataSet extends AbstractJsonDataSet
+	{
+		private static final long serialVersionUID = 1L;
+
+		private transient Reader responseJsonReader;
+
+		public HttpResponseJsonDataSet(Reader responseJsonReader)
+		{
+			super(HttpResponseJsonDataSet.class.getName(), HttpResponseJsonDataSet.class.getName());
+			this.responseJsonReader = responseJsonReader;
+		}
+
+		public HttpResponseJsonDataSet(List<DataSetProperty> properties, Reader responseJsonReader)
+		{
+			super(HttpResponseJsonDataSet.class.getName(), HttpResponseJsonDataSet.class.getName(), properties);
+			this.responseJsonReader = responseJsonReader;
+		}
+
+		@Override
+		protected TemplateResolvedSource<Reader> getJsonReader(DataSetQuery query) throws Throwable
+		{
+			return new TemplateResolvedSource<>(this.responseJsonReader);
+		}
+	}
+}

+ 597 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/JsonChartPluginPropertiesResolver.java

@@ -0,0 +1,597 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.datagear.analysis.Category;
+import org.datagear.analysis.ChartParam;
+import org.datagear.analysis.ChartPlugin;
+import org.datagear.analysis.DataSign;
+import org.datagear.analysis.Icon;
+import org.datagear.util.IOUtil;
+import org.datagear.util.i18n.Label;
+
+/**
+ * JSON {@linkplain ChartPlugin}属性解析器。
+ * <p>
+ * 此类从JSON解析{@linkplain ChartPlugin}对象的属性:
+ * </p>
+ * <code>
+ * <pre>
+ * {
+ * 	id : "...",
+ * 	nameLabel : "..." 或者 { value : "...", localeValues : { "zh" : "...", "en" : "..." }},
+ * 	descLabel : "..." 或者 { ... },
+ * 	manualLabel : "..." 或者 { ... },
+ * 	icons : "..." 或者 { "LIGHT" : "icons/light.png", "DARK" : "icons/dark.png" },
+ * 	chartParams :  [ { ... }, ... ],
+ * 	dataSigns : [ { ... }, ... ],
+ * 	version : "...",
+ * 	order: 1,
+ * 	category: "..." 或者 {name: "...", ...}
+ * }
+ * </pre>
+ * </code>
+ * <p>
+ * 此类是线程安全的。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class JsonChartPluginPropertiesResolver
+{
+	private ConcurrentMap<String, Locale> _localeCache = new ConcurrentHashMap<>();
+
+	public JsonChartPluginPropertiesResolver()
+	{
+		super();
+	}
+
+	/**
+	 * 从映射表解析并设置{@linkplain ChartPlugin}属性。
+	 * <p>
+	 * 它会进行必要的类型转换。
+	 * </p>
+	 * 
+	 * @param chartPlugin
+	 * @param properties
+	 */
+	public void resolveChartPluginProperties(AbstractChartPlugin chartPlugin, Map<String, ?> properties)
+	{
+		chartPlugin.setId((String) properties.get(ChartPlugin.PROPERTY_ID));
+		chartPlugin.setNameLabel(convertToLabel(properties.get(ChartPlugin.PROPERTY_NAME_LABEL)));
+		chartPlugin.setDescLabel(convertToLabel(properties.get(ChartPlugin.PROPERTY_DESC_LABEL)));
+		chartPlugin.setManualLabel(convertToLabel(properties.get(ChartPlugin.PROPERTY_MANUAL_LABEL)));
+		chartPlugin.setIcons(convertToIcons(properties.get(ChartPlugin.PROPERTY_ICONS)));
+		chartPlugin.setChartParams(convertToChartParams(properties.get(ChartPlugin.PROPERTY_CHART_PARAMS)));
+		chartPlugin.setDataSigns(convertToDataSigns(properties.get(ChartPlugin.PROPERTY_DATA_SIGNS)));
+		chartPlugin.setVersion((String) properties.get(ChartPlugin.PROPERTY_VERSION));
+		chartPlugin.setOrder(convertToInt(properties.get(ChartPlugin.PROPERTY_ORDER), chartPlugin.getOrder()));
+		chartPlugin.setCategory(convertToCategory(properties.get(ChartPlugin.PROPERTY_CATEGORY)));
+	}
+
+	/**
+	 * 从JSON字符串解析并设置{@linkplain ChartPlugin}属性。
+	 * 
+	 * @param chartPlugin
+	 * @param json
+	 * @throws IOException
+	 */
+	public void resolveChartPluginProperties(AbstractChartPlugin chartPlugin, String json) throws IOException
+	{
+		@SuppressWarnings("unchecked")
+		Map<String, Object> properties = JsonSupport.parseNonStardand(json, Map.class);
+		resolveChartPluginProperties(chartPlugin, properties);
+	}
+
+	/**
+	 * 从JSON输入流解析并设置{@linkplain ChartPlugin}属性。
+	 * 
+	 * @param chartPlugin
+	 * @param jsonReader
+	 * @throws IOException
+	 */
+	public void resolveChartPluginProperties(AbstractChartPlugin chartPlugin, Reader jsonReader) throws IOException
+	{
+		String json = null;
+
+		StringWriter writer = null;
+		try
+		{
+			writer = new StringWriter();
+			IOUtil.write(jsonReader, writer);
+		}
+		finally
+		{
+			IOUtil.close(writer);
+		}
+
+		json = writer.toString();
+
+		resolveChartPluginProperties(chartPlugin, json);
+	}
+
+	/**
+	 * 从JSON输入流解析并设置{@linkplain ChartPlugin}属性。
+	 * 
+	 * @param chartPlugin
+	 * @param in
+	 * @param encoding
+	 * @throws IOException
+	 */
+	public void resolveChartPluginProperties(AbstractChartPlugin chartPlugin, InputStream in, String encoding)
+			throws IOException
+	{
+		Reader reader = IOUtil.getReader(in, encoding);
+		resolveChartPluginProperties(chartPlugin, reader);
+	}
+
+	/**
+	 * 将对象转换为{@linkplain Label}。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected Label convertToLabel(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof Label)
+			return (Label) obj;
+		else if (obj instanceof String)
+		{
+			Label label = createLabel();
+			label.setValue((String) obj);
+
+			return label;
+		}
+		else if (obj instanceof Map<?, ?>)
+		{
+			@SuppressWarnings("unchecked")
+			Map<String, ?> map = (Map<String, ?>) obj;
+
+			Label label = createLabel();
+			label.setValue((String) map.get(Label.PROPERTY_VALUE));
+
+			Object localeValuesObj = map.get(Label.PROPERTY_LOCALE_VALUES);
+			if (localeValuesObj != null)
+			{
+				Map<Locale, String> localeValues = new HashMap<>();
+
+				@SuppressWarnings("unchecked")
+				Map<String, String> stringLocaleValues = (Map<String, String>) localeValuesObj;
+
+				for (Map.Entry<String, String> entry : stringLocaleValues.entrySet())
+				{
+					Locale locale = this._localeCache.get(entry.getKey());
+					if (locale == null)
+					{
+						locale = stringToLocale(entry.getKey());
+						this._localeCache.putIfAbsent(entry.getKey(), locale);
+					}
+
+					localeValues.put(locale, entry.getValue());
+				}
+
+				label.setLocaleValues(localeValues);
+			}
+
+			return label;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ Label.class.getName() + "] is not supported");
+	}
+
+	/**
+	 * 将对象转换为{@linkplain Icon}映射表。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected Map<String, Icon> convertToIcons(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof String)
+		{
+			Map<String, Icon> icons = new HashMap<>();
+			icons.put(ChartPlugin.DEFAULT_ICON_THEME_NAME, convertToIcon(obj));
+
+			return icons;
+		}
+		else if (obj instanceof Map<?, ?>)
+		{
+			Map<String, Icon> icons = new HashMap<>();
+
+			Map<?, ?> map = (Map<?, ?>) obj;
+
+			for (Map.Entry<?, ?> entry : map.entrySet())
+			{
+				Object key = entry.getKey();
+
+				String themeName = (key instanceof String ? (String) key : key.toString());
+				Icon icon = convertToIcon(entry.getValue());
+
+				icons.put(themeName, icon);
+			}
+
+			return icons;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ Icon.class.getName() + "] map is not supported");
+	}
+
+	/**
+	 * 将对象转换为{@linkplain Icon}。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected Icon convertToIcon(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof Icon)
+			return (Icon) obj;
+		else if (obj instanceof String)
+		{
+			LocationIcon icon = createLocationIcon();
+			icon.setLocation((String) obj);
+
+			return icon;
+		}
+		else if (obj instanceof Map<?, ?>)
+		{
+			@SuppressWarnings("unchecked")
+			Map<String, ?> map = (Map<String, String>) obj;
+
+			String location = (String) map.get(LocationIcon.PROPERTY_LOCATION);
+
+			if (location == null)
+				return null;
+
+			LocationIcon icon = createLocationIcon();
+			icon.setLocation(location);
+
+			return icon;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ Icon.class.getName() + "] is not supported");
+	}
+
+	/**
+	 * 将对象转换为{@linkplain ChartParam}s。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected List<ChartParam> convertToChartParams(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof Object[])
+		{
+			Object[] array = (Object[]) obj;
+
+			List<ChartParam> chartParams = new ArrayList<>();
+
+			for (Object ele : array)
+			{
+				ChartParam chartParam = convertToChartParam(ele);
+
+				if (chartParam != null)
+					chartParams.add(chartParam);
+			}
+
+			if (chartParams.isEmpty())
+				return null;
+
+			return chartParams;
+		}
+		else if (obj instanceof Collection<?>)
+		{
+			Collection<?> collection = (Collection<?>) obj;
+			Object[] array = new Object[collection.size()];
+			collection.toArray(array);
+
+			return convertToChartParams(array);
+		}
+		else
+		{
+			Object[] array = new Object[] { obj };
+
+			return convertToChartParams(array);
+		}
+	}
+
+	/**
+	 * 将对象转换为{@linkplain ChartParam}。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected ChartParam convertToChartParam(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof ChartParam)
+			return (ChartParam) obj;
+		else if (obj instanceof Map<?, ?>)
+		{
+			@SuppressWarnings("unchecked")
+			Map<String, ?> map = (Map<String, ?>) obj;
+
+			String name = (String) map.get(ChartParam.PROPERTY_NAME);
+			if (name == null || name.isEmpty())
+				return null;
+
+			ChartParam chartParam = createChartParam();
+			chartParam.setName(name);
+
+			chartParam.setType(convertToChartParamDataType(map.get(ChartParam.PROPERTY_TYPE)));
+			chartParam.setNameLabel(convertToLabel(map.get(ChartParam.PROPERTY_NAME_LABEL)));
+			chartParam.setDescLabel(convertToLabel(map.get(ChartParam.PROPERTY_DESC_LABEL)));
+
+			return chartParam;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ ChartParam.class.getName() + "] is not supported");
+	}
+
+	protected String convertToChartParamDataType(Object obj)
+	{
+		if (obj instanceof String)
+			return (String) obj;
+		else
+			return ChartParam.DataType.STRING;
+	}
+
+	/**
+	 * 将对象转换为指定枚举类型的对象。
+	 * 
+	 * @param obj
+	 * @param enumType
+	 * @return
+	 */
+	@SuppressWarnings("unchecked")
+	protected <T extends Enum<T>> T convertToEnum(Object obj, Class<T> enumType)
+	{
+		if (obj == null)
+			return null;
+		else if (enumType.isAssignableFrom(obj.getClass()))
+			return (T) obj;
+		else if (obj instanceof String)
+		{
+			String strVal = (String) obj;
+
+			EnumSet<T> enumSet = EnumSet.allOf(enumType);
+
+			for (T e : enumSet)
+			{
+				if (e.name().equalsIgnoreCase(strVal))
+					return e;
+			}
+
+			return null;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ enumType.getName() + "] is not supported");
+	}
+
+	/**
+	 * 将对象转换为{@linkplain DataSigns}。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected List<DataSign> convertToDataSigns(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof Object[])
+		{
+			Object[] array = (Object[]) obj;
+
+			List<DataSign> dataSigns = new ArrayList<>();
+
+			for (Object ele : array)
+			{
+				DataSign dataSign = convertToDataSign(ele);
+
+				if (dataSign != null)
+					dataSigns.add(dataSign);
+			}
+
+			if (dataSigns.isEmpty())
+				return null;
+
+			return dataSigns;
+		}
+		else if (obj instanceof Collection<?>)
+		{
+			Collection<?> collection = (Collection<?>) obj;
+			Object[] array = new Object[collection.size()];
+			collection.toArray(array);
+
+			return convertToDataSigns(array);
+		}
+		else
+		{
+			Object[] array = new Object[] { obj };
+
+			return convertToDataSigns(array);
+		}
+	}
+
+	/**
+	 * 将对象转换为{@linkplain DataSign}。
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	protected DataSign convertToDataSign(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof DataSign)
+			return (DataSign) obj;
+		else if (obj instanceof Map<?, ?>)
+		{
+			@SuppressWarnings("unchecked")
+			Map<String, ?> map = (Map<String, ?>) obj;
+
+			String name = (String) map.get(DataSign.PROPERTY_NAME);
+			if (name == null || name.isEmpty())
+				return null;
+
+			DataSign dataSign = createDataSign();
+			dataSign.setName(name);
+
+			dataSign.setRequired(convertToBoolean(map.get(DataSign.PROPERTY_REQUIRED), true));
+			dataSign.setMultiple(convertToBoolean(map.get(DataSign.PROPERTY_MULTIPLE), true));
+			dataSign.setNameLabel(convertToLabel(map.get(DataSign.PROPERTY_NAME_LABEL)));
+			dataSign.setDescLabel(convertToLabel(map.get(DataSign.PROPERTY_DESC_LABEL)));
+
+			return dataSign;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ DataSign.class.getName() + "] is not supported");
+	}
+
+	protected Category convertToCategory(Object obj)
+	{
+		if (obj == null)
+			return null;
+		else if (obj instanceof Category)
+			return (Category) obj;
+		else if (obj instanceof String)
+			return new Category((String) obj);
+		else if (obj instanceof Map<?, ?>)
+		{
+			@SuppressWarnings("unchecked")
+			Map<String, ?> map = (Map<String, ?>) obj;
+
+			String name = (String) map.get(DataSign.PROPERTY_NAME);
+			if (name == null)
+				return null;
+
+			Category category = createCategory();
+			category.setName(name);
+
+			category.setNameLabel(convertToLabel(map.get(Category.PROPERTY_NAME_LABEL)));
+			category.setDescLabel(convertToLabel(map.get(Category.PROPERTY_DESC_LABEL)));
+			category.setOrder(convertToInt(map.get(Category.PROPERTY_ORDER), category.getOrder()));
+
+			return category;
+		}
+		else
+			throw new UnsupportedOperationException("Convert object of type [" + obj.getClass().getName() + "] to ["
+					+ Category.class.getName() + "] is not supported");
+	}
+
+	/**
+	 * 将对象转换为布尔值。
+	 * 
+	 * @param obj
+	 * @param defaultValue
+	 * @return
+	 */
+	protected boolean convertToBoolean(Object obj, boolean defaultValue)
+	{
+		if (obj == null)
+			return defaultValue;
+		else if (obj instanceof Boolean)
+			return ((Boolean) obj).booleanValue();
+		else if (obj instanceof String)
+		{
+			String str = (String) obj;
+			return ("1".equals(str) || "true".equalsIgnoreCase(str));
+		}
+		else
+			throw new UnsupportedOperationException(
+					"Convert object [" + obj + "] to [" + boolean.class.getName() + "] is not supported");
+	}
+
+	protected int convertToInt(Object obj, int defaultValue)
+	{
+		if (obj == null)
+			return defaultValue;
+		else if (obj instanceof Number)
+			return ((Number) obj).intValue();
+		else if (obj instanceof String)
+		{
+			try
+			{
+				return Integer.parseInt((String) obj);
+			}
+			catch (Exception e)
+			{
+				return defaultValue;
+			}
+		}
+		else
+			throw new UnsupportedOperationException(
+					"Convert object [" + obj + "] to [" + Integer.class.getName() + "] is not supported");
+	}
+
+	/**
+	 * 字符串转换为{@linkplain Locale}。
+	 * 
+	 * @param str
+	 * @return
+	 */
+	protected Locale stringToLocale(String str)
+	{
+		return Label.toLocale(str);
+	}
+
+	protected Label createLabel()
+	{
+		return new Label();
+	}
+
+	protected LocationIcon createLocationIcon()
+	{
+		return new LocationIcon();
+	}
+
+	protected ChartParam createChartParam()
+	{
+		return new ChartParam();
+	}
+
+	protected DataSign createDataSign()
+	{
+		return new DataSign();
+	}
+
+	protected Category createCategory()
+	{
+		return new Category();
+	}
+}

+ 84 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java

@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.util.List;
+
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.FileUtil;
+
+/**
+ * 目录内JSON文件{@linkplain DataSet}。
+ * <p>
+ * 注意:此类不支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class JsonDirectoryFileDataSet extends AbstractJsonFileDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	/** JSON文件所在的目录 */
+	private File directory;
+
+	/** JSON文件名 */
+	private String fileName;
+
+	public JsonDirectoryFileDataSet()
+	{
+		super();
+	}
+
+	public JsonDirectoryFileDataSet(String id, String name, File directory, String fileName)
+	{
+		super(id, name);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	public JsonDirectoryFileDataSet(String id, String name, List<DataSetProperty> properties, File directory,
+			String fileName)
+	{
+		super(id, name, properties);
+		this.directory = directory;
+		this.fileName = fileName;
+	}
+
+	public File getDirectory()
+	{
+		return directory;
+	}
+
+	public void setDirectory(File directory)
+	{
+		this.directory = directory;
+	}
+
+	public String getFileName()
+	{
+		return fileName;
+	}
+
+	public void setFileName(String fileName)
+	{
+		this.fileName = fileName;
+	}
+
+	@Override
+	protected File getJsonFile(DataSetQuery query) throws DataSetException
+	{
+		File jsonFile = FileUtil.getFile(this.directory, this.fileName);
+		return jsonFile;
+	}
+}

+ 216 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/JsonSupport.java

@@ -0,0 +1,216 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+/**
+ * Json支持类。
+ * <p>
+ * 基于Jackson的{@linkplain ObjectMapper}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class JsonSupport
+{
+	private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+	private static final ObjectMapper OBJECT_MAPPER_NON_STARDAND = new ObjectMapper();
+	static
+	{
+		setWriteJsonFeatures(OBJECT_MAPPER);
+		setWriteJsonFeatures(OBJECT_MAPPER_NON_STARDAND);
+		setReadNonStandardJsonFeatures(OBJECT_MAPPER_NON_STARDAND);
+	}
+
+	public JsonSupport()
+	{
+	}
+
+	/**
+	 * 生成JSON。
+	 * 
+	 * @param value
+	 * @return
+	 * @throws JsonProcessingException
+	 */
+	public static String generate(Object value) throws JsonProcessingException
+	{
+		return OBJECT_MAPPER.writeValueAsString(value);
+	}
+
+	/**
+	 * 生成JSON,如果出现异常,则返回{@code defaultJson}。
+	 * 
+	 * @param value
+	 * @param defaultJson
+	 * @return
+	 */
+	public static String generate(Object value, String defaultJson)
+	{
+		try
+		{
+			return generate(value);
+		}
+		catch (JsonProcessingException e)
+		{
+			return defaultJson;
+		}
+	}
+
+	/**
+	 * 解析标准JSON。
+	 * 
+	 * @param <T>
+	 * @param json
+	 * @param type
+	 * @return
+	 * @throws IOException
+	 */
+	public static <T> T parse(String json, Class<T> type) throws IOException
+	{
+		return OBJECT_MAPPER.readValue(json, type);
+	}
+
+	/**
+	 * 解析标准JSON。
+	 * 
+	 * @param <T>
+	 * @param json
+	 * @param type
+	 * @param defaultValue
+	 * @return
+	 */
+	public static <T> T parse(String json, Class<T> type, T defaultValue)
+	{
+		try
+		{
+			return OBJECT_MAPPER.readValue(json, type);
+		}
+		catch (IOException e)
+		{
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * 解析非标准JSON。
+	 * 
+	 * @param <T>
+	 * @param json
+	 * @param type
+	 * @return
+	 * @throws IOException
+	 */
+	public static <T> T parseNonStardand(String json, Class<T> type) throws IOException
+	{
+		return OBJECT_MAPPER_NON_STARDAND.readValue(json, type);
+	}
+
+	/**
+	 * 解析非标准JSON。
+	 * 
+	 * @param <T>
+	 * @param json
+	 * @param type
+	 * @param defaultValue
+	 * @return
+	 */
+	public static <T> T parseNonStardand(String json, Class<T> type, T defaultValue)
+	{
+		try
+		{
+			return OBJECT_MAPPER_NON_STARDAND.readValue(json, type);
+		}
+		catch (IOException e)
+		{
+			return defaultValue;
+		}
+	}
+
+	/**
+	 * 解析非标准JSON。
+	 * 
+	 * @param <T>
+	 * @param reader
+	 * @param type
+	 * @return
+	 * @throws IOException
+	 */
+	public static <T> T parseNonStardand(Reader reader, Class<T> type) throws IOException
+	{
+		return OBJECT_MAPPER_NON_STARDAND.readValue(reader, type);
+	}
+
+	public static ObjectMapper getObjectMapper()
+	{
+		return OBJECT_MAPPER;
+	}
+
+	public static ObjectMapper getObjectMapperNonStardand()
+	{
+		return OBJECT_MAPPER_NON_STARDAND;
+	}
+
+	/**
+	 * 创建{@linkplain ObjectMapper}实例。
+	 * 
+	 * @return
+	 */
+	public static ObjectMapper create()
+	{
+		return new ObjectMapper();
+	}
+
+	/**
+	 * 设置写JSON特性。
+	 * 
+	 * @param objectMapper
+	 */
+	@SuppressWarnings("deprecation")
+	public static void setWriteJsonFeatures(ObjectMapper objectMapper)
+	{
+		objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, false);
+		// XXX jackson默认输出JSON是会加双引号的,这里保留仅用于标识此项设置是必须的
+		objectMapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
+		objectMapper.setSerializationInclusion(Include.NON_NULL);
+	}
+
+	/**
+	 * 设置读非标准JSON特性。
+	 * <p>
+	 * 允许单引号、无引号字段名。
+	 * </p>
+	 * 
+	 * @param objectMapper
+	 */
+	public static void setReadNonStandardJsonFeatures(ObjectMapper objectMapper)
+	{
+		objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+		objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
+	}
+
+	/**
+	 * 禁用{@linkplain JsonGenerator.Feature.AUTO_CLOSE_TARGET}特性。
+	 * 
+	 * @param objectMapper
+	 */
+	public static void disableAutoCloseTargetFeature(ObjectMapper objectMapper)
+	{
+		objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+	}
+}

+ 73 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/JsonValueDataSet.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Reader;
+import java.util.List;
+
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.util.IOUtil;
+
+/**
+ * JSON字符串值数据集。
+ * <p>
+ * 此类的{@linkplain #getValue()}支持<code>Freemarker</code>模板语言。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class JsonValueDataSet extends AbstractJsonDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	private String value;
+
+	public JsonValueDataSet()
+	{
+		super();
+	}
+
+	public JsonValueDataSet(String id, String name, String value)
+	{
+		super(id, name);
+		this.value = value;
+	}
+
+	public JsonValueDataSet(String id, String name, List<DataSetProperty> properties, String value)
+	{
+		super(id, name, properties);
+		this.value = value;
+	}
+
+	public String getValue()
+	{
+		return value;
+	}
+
+	public void setValue(String value)
+	{
+		this.value = value;
+	}
+
+	@Override
+	public TemplateResolvedDataSetResult resolve(DataSetQuery query)
+			throws DataSetException
+	{
+		return (TemplateResolvedDataSetResult) super.resolve(query);
+	}
+
+	@Override
+	protected TemplateResolvedSource<Reader> getJsonReader(DataSetQuery query) throws Throwable
+	{
+		String json = resolveJsonAsTemplate(this.value, query);
+		return new TemplateResolvedSource<>(IOUtil.getReader(json), json);
+	}
+}

+ 79 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/LocationIcon.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.datagear.analysis.Icon;
+import org.datagear.util.FileUtil;
+
+/**
+ * 位置图标。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class LocationIcon extends LocationResource implements Icon
+{
+	private static final long serialVersionUID = 1L;
+
+	public static final String PROPERTY_LOCATION = "location";
+
+	private String type = "";
+
+	private long lastModified = System.currentTimeMillis();
+
+	public LocationIcon()
+	{
+	}
+
+	public LocationIcon(String location)
+	{
+		super(location);
+		this.type = FileUtil.getExtension(location);
+	}
+
+	@Override
+	public String getType()
+	{
+		return type;
+	}
+
+	public void setType(String type)
+	{
+		this.type = type;
+	}
+
+	@Override
+	public long getLastModified()
+	{
+		String location = getLocation();
+
+		if (LocationResource.isFileLocation(location))
+		{
+			File file = getLocationFile(location);
+
+			return file.lastModified();
+		}
+		else
+			return lastModified;
+	}
+
+	public void setLastModified(long lastModified)
+	{
+		this.lastModified = lastModified;
+	}
+
+	@Override
+	public InputStream getInputStream() throws IOException
+	{
+		return super.getInputStream();
+	}
+}

+ 149 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/LocationResource.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+
+import org.datagear.util.FileUtil;
+
+/**
+ * 位置资源。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class LocationResource implements Serializable
+{
+	private static final long serialVersionUID = 1L;
+
+	/** 类路径资源前缀 */
+	public static final String PREFIX_CLASSPATH = "classpath:";
+
+	/** 文件路径资源前缀 */
+	public static final String PREFIX_FILE = "file:";
+
+	private String location;
+
+	public LocationResource()
+	{
+	}
+
+	public LocationResource(String location)
+	{
+		super();
+		this.location = location;
+	}
+
+	public String getLocation()
+	{
+		return location;
+	}
+
+	public void setLocation(String location)
+	{
+		this.location = location;
+	}
+
+	/**
+	 * 获取资源输入流。
+	 * 
+	 * @return
+	 * @throws IOException
+	 */
+	public InputStream getInputStream() throws IOException
+	{
+		return getLocationInputStream(this.location);
+	}
+
+	/**
+	 * 获取指定位置的输入流。
+	 * 
+	 * @param location
+	 * @return
+	 * @throws IOException
+	 */
+	protected InputStream getLocationInputStream(String location) throws IOException
+	{
+		if (location.startsWith(PREFIX_FILE))
+		{
+			File file = getLocationFile(location);
+			return new FileInputStream(file);
+		}
+		else if (location.startsWith(PREFIX_CLASSPATH))
+		{
+			location = location.substring(PREFIX_CLASSPATH.length());
+
+			return getClass().getClassLoader().getResourceAsStream(location);
+		}
+		else
+			throw new UnsupportedOperationException("Location [" + location + "] is not supported");
+	}
+
+	protected File getLocationFile(String fileLocation)
+	{
+		fileLocation = fileLocation.substring(PREFIX_FILE.length());
+
+		File file = FileUtil.getFile(fileLocation);
+
+		return file;
+	}
+
+	/**
+	 * 是否是文件路径位置。
+	 * 
+	 * @param location
+	 * @return
+	 */
+	public static boolean isFileLocation(String location)
+	{
+		if (location == null)
+			return false;
+
+		return location.startsWith(PREFIX_FILE);
+	}
+
+	/**
+	 * 是否是类路径位置。
+	 * 
+	 * @param location
+	 * @return
+	 */
+	public static boolean isClasspathLocation(String location)
+	{
+		if (location == null)
+			return false;
+
+		return location.startsWith(PREFIX_CLASSPATH);
+	}
+
+	/**
+	 * 将文件路径转换为位置。
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public static String toFileLocation(String path)
+	{
+		return PREFIX_FILE + path;
+	}
+
+	/**
+	 * 将类路径转换为位置。
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public static String toClasspathLocation(String path)
+	{
+		return PREFIX_CLASSPATH + path;
+	}
+}

+ 130 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/NameAsTemplateDashboardWidgetResManager.java

@@ -0,0 +1,130 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.List;
+
+import org.datagear.analysis.TemplateDashboardWidget;
+import org.datagear.analysis.TemplateDashboardWidgetResManager;
+import org.datagear.util.IOUtil;
+import org.datagear.util.StringUtil;
+
+/**
+ * 将资源名称作为资源内容的{@linkplain TemplateDashboardWidgetResManager}。
+ * <p>
+ * 此类的:
+ * </p>
+ * <p>
+ * {@linkplain #exists(String, String)}始终返回{@code true};
+ * </p>
+ * <p>
+ * {@linkplain #getReader(TemplateDashboardWidget, String)}、{@linkplain #getReader(String, String, String)}始终返回由资源名称构建的输入流;
+ * </p>
+ * <p>
+ * {@linkplain #lastModified(String, String)}始终返回{@code 0};
+ * </p>
+ * <p>
+ * {@linkplain #list(String)}始终返回空列表;
+ * </p>
+ * <p>
+ * 其他方法直接抛出{@linkplain UnsupportedOperationException}。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class NameAsTemplateDashboardWidgetResManager extends AbstractTemplateDashboardWidgetResManager
+{
+	public NameAsTemplateDashboardWidgetResManager()
+	{
+		super();
+	}
+
+	@Override
+	public boolean exists(String id, String name)
+	{
+		return true;
+	}
+
+	@Override
+	public Reader getReader(String id, String name, String encoding) throws IOException
+	{
+		String content = name;
+		if (StringUtil.isEmpty(content))
+			content = "";
+
+		return IOUtil.getReader(content);
+	}
+
+	@Override
+	public Writer getWriter(String id, String name, String encoding) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public InputStream getInputStream(String id, String name) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public OutputStream getOutputStream(String id, String name) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void copyFrom(String id, File directory) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void copyTo(String id, File directory) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void copyTo(String sourceId, String targetId) throws IOException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public long lastModified(String id, String name)
+	{
+		return 0;
+	}
+
+	@Override
+	public List<String> list(String id)
+	{
+		return Collections.emptyList();
+	}
+
+	@Override
+	public void delete(String id)
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public void delete(String id, String name)
+	{
+		throw new UnsupportedOperationException();
+	}
+}

+ 57 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/NotNameValueObjArrayJsonException.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import org.datagear.analysis.DataSetException;
+
+/**
+ * JSON字符串不是名/值数组格式异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class NotNameValueObjArrayJsonException extends DataSetException
+{
+	private static final long serialVersionUID = 1L;
+
+	private String json;
+
+	public NotNameValueObjArrayJsonException(String json)
+	{
+		super("The json must be name/value object array");
+		this.json = json;
+	}
+
+	public NotNameValueObjArrayJsonException(String json, String message)
+	{
+		super(message);
+		this.json = json;
+	}
+
+	public NotNameValueObjArrayJsonException(String json, Throwable cause)
+	{
+		super(cause);
+		this.json = json;
+	}
+
+	public NotNameValueObjArrayJsonException(String json, String message, Throwable cause)
+	{
+		super(message, cause);
+		this.json = json;
+	}
+
+	public String getJson()
+	{
+		return json;
+	}
+
+	protected void setJson(String json)
+	{
+		this.json = json;
+	}
+}

+ 73 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ProfileDataSet.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.util.List;
+
+import org.datagear.analysis.DataSet;
+import org.datagear.analysis.DataSetException;
+import org.datagear.analysis.DataSetProperty;
+import org.datagear.analysis.DataSetQuery;
+import org.datagear.analysis.DataSetResult;
+
+/**
+ * 轮廓{@linkplain DataSet}。
+ * <p>
+ * 此类仅用于描述{@linkplain DataSet}接口数据结构,不包含任何其他逻辑。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ProfileDataSet extends AbstractDataSet
+{
+	private static final long serialVersionUID = 1L;
+
+	public ProfileDataSet()
+	{
+	}
+
+	public ProfileDataSet(String id, String name, List<DataSetProperty> properties)
+	{
+		super(id, name, properties);
+	}
+
+	public ProfileDataSet(DataSet dataSet)
+	{
+		super(dataSet.getId(), dataSet.getName(), dataSet.getProperties());
+		setMutableModel(dataSet.isMutableModel());
+		setParams(dataSet.getParams());
+	}
+
+	@Override
+	public DataSetResult getResult(DataSetQuery query) throws DataSetException
+	{
+		throw new UnsupportedOperationException();
+	}
+
+	/**
+	 * 构建{@linkplain ProfileDataSet}。
+	 * <p>
+	 * 如果{@code dataSet}是{@linkplain ProfileDataSet}实例,将直接返回。
+	 * </p>
+	 * 
+	 * @param dataSet
+	 *            允许为{@code null}
+	 * @return
+	 */
+	public static ProfileDataSet valueOf(DataSet dataSet)
+	{
+		if (dataSet == null)
+			return null;
+
+		if (dataSet instanceof ProfileDataSet)
+			return (ProfileDataSet) dataSet;
+
+		return new ProfileDataSet(dataSet);
+	}
+}

+ 465 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java

@@ -0,0 +1,465 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.datagear.util.StringUtil;
+
+/**
+ * 范围解析器。
+ * <p>
+ * 此类用于解析诸如{@code "1, 2-3, 6-, -15}之类的范围表达式。
+ * </p>
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class RangeExpResolver
+{
+	public static final char RANGE_SPLITTER_CHAR = '-';
+
+	public static final String RANGE_SPLITTER_STRING = "-";
+
+	public static final char RANGE_GROUP_SPLITTER_CHAR = ',';
+
+	public static final String RANGE_GROUP_SPLITTER_STRING = ",";
+
+	private char rangeSplitter = RANGE_SPLITTER_CHAR;
+
+	private char rangeGroupSplitter = RANGE_GROUP_SPLITTER_CHAR;
+
+	public RangeExpResolver()
+	{
+		super();
+	}
+
+	public char getRangeSplitter()
+	{
+		return rangeSplitter;
+	}
+
+	public void setRangeSplitter(char rangeSplitter)
+	{
+		this.rangeSplitter = rangeSplitter;
+	}
+
+	public char getRangeGroupSplitter()
+	{
+		return rangeGroupSplitter;
+	}
+
+	public void setRangeGroupSplitter(char rangeGroupSplitter)
+	{
+		this.rangeGroupSplitter = rangeGroupSplitter;
+	}
+
+	@SuppressWarnings("unchecked")
+	public List<IndexRange> resolveIndex(String exp) throws NumberFormatException
+	{
+		List<Range> ranges = resolve(exp);
+
+		if (ranges.isEmpty())
+			return Collections.EMPTY_LIST;
+
+		List<IndexRange> indexRanges = new ArrayList<>(ranges.size());
+
+		for (Range range : ranges)
+			indexRanges.add(new IndexRange(range));
+
+		return indexRanges;
+	}
+
+	/**
+	 * 解析范围表达式组。
+	 * <p>
+	 * 例如:{@code "1, 2-5, 8-, -15"}
+	 * </p>
+	 * 
+	 * @param exp
+	 * @return 如果{@code exp}为{@code null}、或{@code ""},将返回空列表
+	 */
+	@SuppressWarnings("unchecked")
+	public List<Range> resolve(String exp)
+	{
+		if (exp != null)
+			exp = exp.trim();
+
+		if (exp == null || exp.isEmpty())
+			return Collections.EMPTY_LIST;
+
+		List<Range> ranges = new ArrayList<>();
+
+		String[] ss = StringUtil.split(exp, this.rangeGroupSplitter + "", false);
+
+		for (String s : ss)
+		{
+			Range range = resolveSingle(s);
+
+			if (range != null)
+				ranges.add(range);
+		}
+
+		return ranges;
+	}
+
+	public IndexRange resolveSingleIndex(String exp) throws NumberFormatException
+	{
+		Range range = resolveSingle(exp);
+
+		if (range == null)
+			return null;
+
+		return new IndexRange(range);
+	}
+
+	/**
+	 * 解析单个范围表达式。
+	 * <p>
+	 * 例如:{@code "1"}、{@code "4-5"}、{@code "8-"}、{@code "-15"}
+	 * </p>
+	 * 
+	 * @param exp
+	 * @return 如果{@code exp}为{@code null}、或{@code ""},将返回{@code null}
+	 */
+	public Range resolveSingle(String exp)
+	{
+		if (exp != null)
+			exp = exp.trim();
+
+		if (exp == null || exp.isEmpty())
+			return null;
+
+		String from = "";
+		String to = "";
+
+		int idx = exp.indexOf(this.rangeSplitter);
+		int len = exp.length();
+
+		// 单个值:"1"
+		if (idx < 0)
+		{
+			from = exp;
+			to = from;
+		}
+		// 都未指定:"-"
+		else if (idx == 0 && len == 1)
+		{
+			from = "";
+			to = "";
+		}
+		// 仅指定截至值:"-4"
+		else if (idx == 0)
+		{
+			from = "";
+			to = exp.substring(1);
+		}
+		// 仅指定起始值:"4-"
+		else if (idx == len - 1)
+		{
+			from = exp.substring(0, len - 1);
+			to = "";
+		}
+		// 都指定:"3-5"
+		else
+		{
+			from = exp.substring(0, idx);
+			to = exp.substring(idx + 1);
+		}
+
+		return new Range(from, to);
+	}
+
+	public static RangeExpResolver valueOf(char rangeSplitter, char rangeGroupSplitter)
+	{
+		RangeExpResolver resolver = new RangeExpResolver();
+		resolver.setRangeSplitter(rangeSplitter);
+		resolver.setRangeGroupSplitter(rangeGroupSplitter);
+
+		return resolver;
+	}
+
+	/**
+	 * 范围。
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	public static class Range implements Serializable
+	{
+		private static final long serialVersionUID = 1L;
+
+		/** 起始 */
+		private String from = "";
+
+		/** 截至 */
+		private String to = "";
+
+		public Range()
+		{
+			super();
+		}
+
+		public Range(String from)
+		{
+			super();
+			this.from = from;
+		}
+
+		public Range(String from, String to)
+		{
+			super();
+			this.from = from;
+			this.to = to;
+		}
+
+		public boolean hasFrom()
+		{
+			return (this.from != null && !this.from.isEmpty());
+		}
+
+		public String trimFrom()
+		{
+			if (this.from == null)
+				return "";
+
+			return this.from.trim();
+		}
+
+		public String getFrom()
+		{
+			return from;
+		}
+
+		public void setFrom(String from)
+		{
+			this.from = from;
+		}
+
+		public boolean hasTo()
+		{
+			return (this.to != null && !this.to.isEmpty());
+		}
+
+		public String trimTo()
+		{
+			if (this.to == null)
+				return "";
+
+			return this.to.trim();
+		}
+
+		public String getTo()
+		{
+			return to;
+		}
+
+		public void setTo(String to)
+		{
+			this.to = to;
+		}
+
+		@Override
+		public int hashCode()
+		{
+			final int prime = 31;
+			int result = 1;
+			result = prime * result + ((from == null) ? 0 : from.hashCode());
+			result = prime * result + ((to == null) ? 0 : to.hashCode());
+			return result;
+		}
+
+		@Override
+		public boolean equals(Object obj)
+		{
+			if (this == obj)
+				return true;
+			if (obj == null)
+				return false;
+			if (getClass() != obj.getClass())
+				return false;
+			Range other = (Range) obj;
+			if (from == null)
+			{
+				if (other.from != null)
+					return false;
+			}
+			else if (!from.equals(other.from))
+				return false;
+			if (to == null)
+			{
+				if (other.to != null)
+					return false;
+			}
+			else if (!to.equals(other.to))
+				return false;
+			return true;
+		}
+
+		@Override
+		public String toString()
+		{
+			return getClass().getSimpleName() + " [from=" + from + ", to=" + to + "]";
+		}
+	}
+
+	/**
+	 * 索引范围。
+	 * <p>
+	 * 索引指大于或等于{@code 0}的整数值。
+	 * </p>
+	 * 
+	 * @author datagear@163.com
+	 *
+	 */
+	protected static class IndexRange implements Serializable
+	{
+		private static final long serialVersionUID = 1L;
+
+		/** 起始索引 */
+		private int from = 0;
+
+		/** 截至索引(包含) */
+		private int to = -1;
+
+		public IndexRange()
+		{
+			super();
+			this.from = 0;
+			this.to = -1;
+		}
+
+		public IndexRange(int from)
+		{
+			super();
+			this.from = from;
+			this.to = -1;
+		}
+
+		public IndexRange(int from, int to)
+		{
+			super();
+			this.from = from;
+			this.to = to;
+		}
+
+		public IndexRange(Range range) throws NumberFormatException
+		{
+			super();
+
+			int from = 0;
+			int to = -1;
+
+			String fromStr = range.trimFrom();
+			String toStr = range.trimTo();
+
+			if (!StringUtil.isEmpty(fromStr))
+				from = Integer.parseInt(fromStr);
+
+			if (!StringUtil.isEmpty(toStr))
+				to = Integer.parseInt(toStr);
+
+			this.from = from;
+			this.to = to;
+		}
+
+		public int getFrom()
+		{
+			return from;
+		}
+
+		/**
+		 * 设置起始索引。
+		 * 
+		 * @param from
+		 *            起始索引,小于{@code 0}表示不限定
+		 */
+		public void setFrom(int from)
+		{
+			this.from = from;
+		}
+
+		public int getTo()
+		{
+			return to;
+		}
+
+		/**
+		 * 设置截至索引(包含)。
+		 * 
+		 * @param to
+		 *            截至索引,小于{@code 0}表示不限定
+		 */
+		public void setTo(int to)
+		{
+			this.to = to;
+		}
+
+		/**
+		 * 是否包含给定索引数值。
+		 * 
+		 * @param index
+		 * @return
+		 */
+		public boolean includes(int index)
+		{
+			if (this.from > -1 && index < this.from)
+				return false;
+
+			if (this.to > -1 && index > this.to)
+				return false;
+
+			return true;
+		}
+
+		@Override
+		public int hashCode()
+		{
+			final int prime = 31;
+			int result = 1;
+			result = prime * result + from;
+			result = prime * result + to;
+			return result;
+		}
+
+		@Override
+		public boolean equals(Object obj)
+		{
+			if (this == obj)
+				return true;
+			if (obj == null)
+				return false;
+			if (getClass() != obj.getClass())
+				return false;
+			IndexRange other = (IndexRange) obj;
+			if (from != other.from)
+				return false;
+			if (to != other.to)
+				return false;
+			return true;
+		}
+
+		@Override
+		public String toString()
+		{
+			return getClass().getSimpleName() + " [from=" + from + ", to=" + to + "]";
+		}
+
+		public static boolean includes(List<IndexRange> indexRanges, int index)
+		{
+			for (int i = 0; i < indexRanges.size(); i++)
+				if (indexRanges.get(i).includes(index))
+					return true;
+
+			return false;
+		}
+	}
+}

+ 55 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/ReadJsonDataPathException.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+/**
+ * 读取指定JSON路径的数据异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class ReadJsonDataPathException extends DataSetSourceParseException
+{
+	private static final long serialVersionUID = 1L;
+
+	private String dataPath;
+
+	public ReadJsonDataPathException(String dataPath)
+	{
+		super();
+		this.dataPath = dataPath;
+	}
+
+	public ReadJsonDataPathException(String dataPath, String message)
+	{
+		super(message);
+		this.dataPath = dataPath;
+	}
+
+	public ReadJsonDataPathException(String dataPath, Throwable cause)
+	{
+		super(cause);
+		this.dataPath = dataPath;
+	}
+
+	public ReadJsonDataPathException(String dataPath, String message, Throwable cause)
+	{
+		super(message, cause);
+		this.dataPath = dataPath;
+	}
+
+	public String getDataPath()
+	{
+		return dataPath;
+	}
+
+	protected void setDataPath(String dataPath)
+	{
+		this.dataPath = dataPath;
+	}
+}

+ 39 - 0
datagear-analysis/src/main/java/org/datagear/analysis/support/RequestContentNotNameValueObjArrayJsonException.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 datagear.tech
+ *
+ * Licensed under the LGPLv3 license:
+ * http://www.gnu.org/licenses/lgpl-3.0.html
+ */
+
+package org.datagear.analysis.support;
+
+/**
+ * {@linkplain HttpDataSet#getRequestContent()}不是名/值对象数组JSON异常。
+ * 
+ * @author datagear@163.com
+ *
+ */
+public class RequestContentNotNameValueObjArrayJsonException extends NotNameValueObjArrayJsonException
+{
+	private static final long serialVersionUID = 1L;
+
+	public RequestContentNotNameValueObjArrayJsonException(String json)
+	{
+		super(json);
+	}
+
+	public RequestContentNotNameValueObjArrayJsonException(String json, String message)
+	{
+		super(json, message);
+	}
+
+	public RequestContentNotNameValueObjArrayJsonException(String json, Throwable cause)
+	{
+		super(json, cause);
+	}
+
+	public RequestContentNotNameValueObjArrayJsonException(String json, String message, Throwable cause)
+	{
+		super(json, message, cause);
+	}
+}

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